diff --git a/.editorconfig b/.editorconfig index 4b1bbb8277..323cedc03c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ indent_style = space # XML project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 2 +indent_size = 4 # XML config files [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] @@ -105,6 +105,10 @@ dotnet_naming_symbols.async_methods.required_modifiers = async # Don't force namespaces to match their folder names dotnet_diagnostic.IDE0130.severity = none +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf ############################### # C# Coding Conventions # @@ -180,3 +184,8 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true +csharp_using_directive_placement = inside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_local_functions = false:warning diff --git a/DSharpPlus.Rest/DiscordRestClient.cs b/DSharpPlus.Rest/DiscordRestClient.cs index cec17bd1ab..c12b9fbea2 100644 --- a/DSharpPlus.Rest/DiscordRestClient.cs +++ b/DSharpPlus.Rest/DiscordRestClient.cs @@ -3,2264 +3,2264 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using DSharpPlus.Entities; using DSharpPlus.Exceptions; using DSharpPlus.Net.Abstractions; using DSharpPlus.Net.Models; -namespace DSharpPlus +namespace DSharpPlus; + +public class DiscordRestClient : BaseDiscordClient { - public class DiscordRestClient : BaseDiscordClient - { - /// - /// Gets the dictionary of guilds cached by this client. - /// - public override IReadOnlyDictionary Guilds - => this._guilds_lazy.Value; + /// + /// Gets the dictionary of guilds cached by this client. + /// + public override IReadOnlyDictionary Guilds + => this._guilds_lazy.Value; - internal Dictionary _guilds = new(); - private Lazy> _guilds_lazy; + internal Dictionary _guilds = new(); + private Lazy> _guilds_lazy; - public DiscordRestClient(DiscordConfiguration config) : base(config) - { - this._disposed = false; - } + public DiscordRestClient(DiscordConfiguration config) : base(config) + { + this._disposed = false; + } - /// - /// Initializes cache - /// - /// - public async Task InitializeCacheAsync() + /// + /// Initializes cache + /// + /// + public async Task InitializeCacheAsync() + { + await base.InitializeAsync(); + this._guilds_lazy = new Lazy>(() => new ReadOnlyDictionary(this._guilds)); + var gs = await this.ApiClient.GetCurrentUserGuildsAsync(100, null, null); + foreach (var g in gs) { - await base.InitializeAsync(); - this._guilds_lazy = new Lazy>(() => new ReadOnlyDictionary(this._guilds)); - var gs = await this.ApiClient.GetCurrentUserGuildsAsync(100, null, null); - foreach (var g in gs) - { - this._guilds[g.Id] = g; - } + this._guilds[g.Id] = g; } + } - #region Scheduled Guild Events - - /// - /// Creates a new scheduled guild event. - /// - /// The guild to create an event on. - /// The name of the event, up to 100 characters. - /// The description of the event, up to 1000 characters. - /// The channel the event will take place in, if applicable. - /// The type of event. If , a end time must be specified. - /// The privacy level of the event. - /// When the event starts. Must be in the future and before the end date, if specified. - /// When the event ends. Required for - /// Where this location takes place. - /// The created event. - public Task CreateScheduledGuildEventAsync(ulong guildId, string name, string description, ulong? channelId, ScheduledGuildEventType type, ScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end, string location = null) - => this.ApiClient.CreateScheduledGuildEventAsync(guildId, name, description, channelId, start, end, type, privacyLevel, new DiscordScheduledGuildEventMetadata(location)); - - /// - /// Delete a scheduled guild event. - /// - /// The ID the guild the event resides on. - /// The ID of the event to delete. - public Task DeleteScheduledGuildEventAsync(ulong guildId, ulong eventId) - => this.ApiClient.DeleteScheduledGuildEventAsync(guildId, eventId); - - /// - /// Gets a specific scheduled guild event. - /// - /// The ID of the guild the event resides on. - /// The ID of the event to get - /// The requested event. - public Task GetScheduledGuildEventAsync(ulong guildId, ulong eventId) - => this.ApiClient.GetScheduledGuildEventAsync(guildId, eventId); - - /// - /// Gets all available scheduled guild events. - /// - /// The ID of the guild to query. - /// All active and scheduled events. - public Task> GetScheduledGuildEventsAsync(ulong guildId) - => this.ApiClient.GetScheduledGuildEventsAsync(guildId); - - - /// - /// Modify a scheduled guild event. - /// - /// The ID of the guild the event resides on. - /// The ID of the event to modify. - /// The action to apply to the event. - /// The modified event. - public Task ModifyScheduledGuildEventAsync(ulong guildId, ulong eventId, Action mdl) + #region Scheduled Guild Events + + /// + /// Creates a new scheduled guild event. + /// + /// The guild to create an event on. + /// The name of the event, up to 100 characters. + /// The description of the event, up to 1000 characters. + /// The channel the event will take place in, if applicable. + /// The type of event. If , a end time must be specified. + /// The privacy level of the event. + /// When the event starts. Must be in the future and before the end date, if specified. + /// When the event ends. Required for + /// Where this location takes place. + /// The created event. + public async Task CreateScheduledGuildEventAsync(ulong guildId, string name, string description, ulong? channelId, ScheduledGuildEventType type, ScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end, string location = null) + => await this.ApiClient.CreateScheduledGuildEventAsync(guildId, name, description, start, type, privacyLevel, new DiscordScheduledGuildEventMetadata(location), end, channelId); + + /// + /// Delete a scheduled guild event. + /// + /// The ID the guild the event resides on. + /// The ID of the event to delete. + public async Task DeleteScheduledGuildEventAsync(ulong guildId, ulong eventId) + => await this.ApiClient.DeleteScheduledGuildEventAsync(guildId, eventId); + + /// + /// Gets a specific scheduled guild event. + /// + /// The ID of the guild the event resides on. + /// The ID of the event to get + /// The requested event. + public async Task GetScheduledGuildEventAsync(ulong guildId, ulong eventId) + => await this.ApiClient.GetScheduledGuildEventAsync(guildId, eventId); + + /// + /// Gets all available scheduled guild events. + /// + /// The ID of the guild to query. + /// All active and scheduled events. + public async Task> GetScheduledGuildEventsAsync(ulong guildId) + => await this.ApiClient.GetScheduledGuildEventsAsync(guildId); + + + /// + /// Modify a scheduled guild event. + /// + /// The ID of the guild the event resides on. + /// The ID of the event to modify. + /// The action to apply to the event. + /// The modified event. + public async Task ModifyScheduledGuildEventAsync(ulong guildId, ulong eventId, Action mdl) + { + var model = new ScheduledGuildEventEditModel(); + mdl(model); + + if (model.Type.HasValue && model.Type.Value is ScheduledGuildEventType.StageInstance or ScheduledGuildEventType.VoiceChannel) + if (!model.Channel.HasValue) + throw new ArgumentException("Channel must be supplied if the event is a stage instance or voice channel event."); + + if (model.Type.HasValue && model.Type.Value is ScheduledGuildEventType.External) { - var model = new ScheduledGuildEventEditModel(); - mdl(model); + if (!model.EndTime.HasValue) + throw new ArgumentException("End must be supplied if the event is an external event."); - if (model.Type.HasValue && model.Type.Value is ScheduledGuildEventType.StageInstance or ScheduledGuildEventType.VoiceChannel) - if (!model.Channel.HasValue) - throw new ArgumentException("Channel must be supplied if the event is a stage instance or voice channel event."); + if (!model.Metadata.HasValue || string.IsNullOrEmpty(model.Metadata.Value.Location)) + throw new ArgumentException("Location must be supplied if the event is an external event."); - if (model.Type.HasValue && model.Type.Value is ScheduledGuildEventType.External) - { - if (!model.EndTime.HasValue) - throw new ArgumentException("End must be supplied if the event is an external event."); + if (model.Channel.HasValue && model.Channel.Value != null) + throw new ArgumentException("Channel must not be supplied if the event is an external event."); + } - if (!model.Metadata.HasValue || string.IsNullOrEmpty(model.Metadata.Value.Location)) - throw new ArgumentException("Location must be supplied if the event is an external event."); + // We only have an ID to work off of, so we have no validation as to the current state of the event. + if (model.Status.HasValue && model.Status.Value is ScheduledGuildEventStatus.Scheduled) + throw new ArgumentException("Status cannot be set to scheduled."); + + return await this.ApiClient.ModifyScheduledGuildEventAsync( + guildId, eventId, + model.Name, model.Description, + model.Channel.IfPresent(c => c?.Id), + model.StartTime, model.EndTime, + model.Type, model.PrivacyLevel, + model.Metadata, model.Status); + } - if (model.Channel.HasValue && model.Channel.Value != null) - throw new ArgumentException("Channel must not be supplied if the event is an external event."); - } + /// + /// Gets the users interested in the guild event. + /// + /// The ID of the guild the event resides on. + /// The ID of the event. + /// How many users to query. + /// Fetch users after this ID. + /// Fetch users before this ID. + /// The users interested in the event. + public async Task> GetScheduledGuildEventUsersAsync(ulong guildId, ulong eventId, int limit = 100, ulong? after = null, ulong? before = null) + { + var remaining = limit; + ulong? last = null; + var isAfter = after != null; - // We only have an ID to work off of, so we have no validation as to the current state of the event. - if (model.Status.HasValue && model.Status.Value is ScheduledGuildEventStatus.Scheduled) - throw new ArgumentException("Status cannot be set to scheduled."); - - return this.ApiClient.ModifyScheduledGuildEventAsync( - guildId, eventId, - model.Name, model.Description, - model.Channel.IfPresent(c => c?.Id), - model.StartTime, model.EndTime, - model.Type, model.PrivacyLevel, - model.Metadata, model.Status); - } + var users = new List(); - /// - /// Gets the users interested in the guild event. - /// - /// The ID of the guild the event resides on. - /// The ID of the event. - /// How many users to query. - /// Fetch users after this ID. - /// Fetch users before this ID. - /// The users interested in the event. - public async Task> GetScheduledGuildEventUsersAsync(ulong guildId, ulong eventId, int limit = 100, ulong? after = null, ulong? before = null) + int lastCount; + do { - var remaining = limit; - ulong? last = null; - var isAfter = after != null; + var fetchSize = remaining > 100 ? 100 : remaining; + var fetch = await this.ApiClient.GetScheduledGuildEventUsersAsync(guildId, eventId, true, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null); - var users = new List(); + lastCount = fetch.Count; + remaining -= lastCount; - int lastCount; - do + if (!isAfter) { - var fetchSize = remaining > 100 ? 100 : remaining; - var fetch = await this.ApiClient.GetScheduledGuildEventUsersAsync(guildId, eventId, true, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null); - - lastCount = fetch.Count; - remaining -= lastCount; - - if (!isAfter) - { - users.AddRange(fetch); - last = fetch.LastOrDefault()?.Id; - } - else - { - users.InsertRange(0, fetch); - last = fetch.FirstOrDefault()?.Id; - } + users.AddRange(fetch); + last = fetch.LastOrDefault()?.Id; } - while (remaining > 0 && lastCount > 0); + else + { + users.InsertRange(0, fetch); + last = fetch.FirstOrDefault()?.Id; + } + } + while (remaining > 0 && lastCount > 0); - return users.AsReadOnly(); - } + return users.AsReadOnly(); + } - #endregion - - #region Guild - - /// - /// Searches the given guild for members who's display name start with the specified name. - /// - /// The ID of the guild to search. - /// The name to search for. - /// The maximum amount of members to return. Max 1000. Defaults to 1. - /// The members found, if any. - public Task> SearchMembersAsync(ulong guild_id, string name, int? limit = 1) - => this.ApiClient.SearchMembersAsync(guild_id, name, limit); - - /// - /// Creates a new guild - /// - /// New guild's name - /// New guild's region ID - /// New guild's icon (base64) - /// New guild's verification level - /// New guild's default message notification level - /// New guild's system channel flags - /// - public Task CreateGuildAsync(string name, string region_id, string iconb64, VerificationLevel? verification_level, DefaultMessageNotifications? default_message_notifications, SystemChannelFlags? system_channel_flags) - => this.ApiClient.CreateGuildAsync(name, region_id, iconb64, verification_level, default_message_notifications, system_channel_flags); - - /// - /// Creates a guild from a template. This requires the bot to be in less than 10 guilds total. - /// - /// The template code. - /// Name of the guild. - /// Stream containing the icon for the guild. - /// The created guild. - public Task CreateGuildFromTemplateAsync(string code, string name, string icon) - => this.ApiClient.CreateGuildFromTemplateAsync(code, name, icon); - - /// - /// Deletes a guild - /// - /// Guild ID - /// - public Task DeleteGuildAsync(ulong id) - => this.ApiClient.DeleteGuildAsync(id); - - /// - /// Modifies a guild - /// - /// Guild ID - /// New guild Name - /// New guild voice region - /// New guild verification level - /// New guild default message notification level - /// New guild MFA level - /// New guild explicit content filter level - /// New guild AFK channel ID - /// New guild AFK timeout in seconds - /// New guild icon (base64) - /// New guild owner ID - /// New guild splash (base64) - /// New guild system channel ID - /// New guild banner - /// New guild description - /// New guild Discovery splash - /// List of new guild features - /// New preferred locale - /// New updates channel ID - /// New rules channel ID - /// New system channel flags - /// Modify reason - /// - public Task ModifyGuildAsync(ulong guild_id, Optional name, - Optional region, Optional verification_level, - Optional default_message_notifications, Optional mfa_level, - Optional explicit_content_filter, Optional afk_channel_id, - Optional afk_timeout, Optional iconb64, Optional owner_id, Optional splashb64, - Optional systemChannelId, Optional banner, Optional description, - Optional discorverySplash, Optional> features, Optional preferredLocale, - Optional publicUpdatesChannelId, Optional rulesChannelId, Optional systemChannelFlags, - string reason) - => this.ApiClient.ModifyGuildAsync(guild_id, name, region, verification_level, default_message_notifications, mfa_level, explicit_content_filter, afk_channel_id, afk_timeout, iconb64, - owner_id, splashb64, systemChannelId, banner, description, discorverySplash, features, preferredLocale, publicUpdatesChannelId, rulesChannelId, systemChannelFlags, reason); - - /// - /// Modifies a guild - /// - /// Guild ID - /// Guild modifications - /// - public async Task ModifyGuildAsync(ulong guild_id, Action action) - { - var mdl = new GuildEditModel(); - action(mdl); - - if (mdl.AfkChannel.HasValue) - if (mdl.AfkChannel.Value.Type != ChannelType.Voice) - throw new ArgumentException("AFK channel needs to be a voice channel!"); - - var iconb64 = Optional.FromNoValue(); - if (mdl.Icon.HasValue && mdl.Icon.Value != null) - using (var imgtool = new ImageTool(mdl.Icon.Value)) - iconb64 = imgtool.GetBase64(); - else if (mdl.Icon.HasValue) - iconb64 = null; - - var splashb64 = Optional.FromNoValue(); - if (mdl.Splash.HasValue && mdl.Splash.Value != null) - using (var imgtool = new ImageTool(mdl.Splash.Value)) - splashb64 = imgtool.GetBase64(); - else if (mdl.Splash.HasValue) - splashb64 = null; - - var bannerb64 = Optional.FromNoValue(); - - if (mdl.Banner.HasValue && mdl.Banner.Value != null) - using (var imgtool = new ImageTool(mdl.Banner.Value)) - bannerb64 = imgtool.GetBase64(); - else if (mdl.Banner.HasValue) - bannerb64 = null; - - return await this.ApiClient.ModifyGuildAsync(guild_id, mdl.Name, mdl.Region.IfPresent(x => x.Id), mdl.VerificationLevel, mdl.DefaultMessageNotifications, - mdl.MfaLevel, mdl.ExplicitContentFilter, mdl.AfkChannel.IfPresent(x => x?.Id), mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(x => x.Id), - splashb64, mdl.SystemChannel.IfPresent(x => x?.Id), bannerb64, mdl.Description, mdl.DiscoverySplash, mdl.Features, mdl.PreferredLocale, - mdl.PublicUpdatesChannel.IfPresent(e => e?.Id), mdl.RulesChannel.IfPresent(e => e?.Id), mdl.SystemChannelFlags, mdl.AuditLogReason); - } + #endregion + + #region Guild + + /// + /// Searches the given guild for members who's display name start with the specified name. + /// + /// The ID of the guild to search. + /// The name to search for. + /// The maximum amount of members to return. Max 1000. Defaults to 1. + /// The members found, if any. + public async Task> SearchMembersAsync(ulong guild_id, string name, int? limit = 1) + => await this.ApiClient.SearchMembersAsync(guild_id, name, limit); + + /// + /// Creates a new guild + /// + /// New guild's name + /// New guild's region ID + /// New guild's icon (base64) + /// New guild's verification level + /// New guild's default message notification level + /// New guild's system channel flags + /// + public async Task CreateGuildAsync(string name, string region_id, string iconb64, VerificationLevel? verification_level, DefaultMessageNotifications? default_message_notifications, SystemChannelFlags? system_channel_flags) + => await this.ApiClient.CreateGuildAsync(name, region_id, iconb64, verification_level, default_message_notifications, system_channel_flags); + + /// + /// Creates a guild from a template. This requires the bot to be in less than 10 guilds total. + /// + /// The template code. + /// Name of the guild. + /// Stream containing the icon for the guild. + /// The created guild. + public async Task CreateGuildFromTemplateAsync(string code, string name, string icon) + => await this.ApiClient.CreateGuildFromTemplateAsync(code, name, icon); + + /// + /// Deletes a guild + /// + /// Guild ID + /// + public async Task DeleteGuildAsync(ulong id) + => await this.ApiClient.DeleteGuildAsync(id); + + /// + /// Modifies a guild + /// + /// Guild ID + /// New guild Name + /// New guild voice region + /// New guild verification level + /// New guild default message notification level + /// New guild MFA level + /// New guild explicit content filter level + /// New guild AFK channel ID + /// New guild AFK timeout in seconds + /// New guild icon (base64) + /// New guild owner ID + /// New guild splash (base64) + /// New guild system channel ID + /// New guild banner + /// New guild description + /// New guild Discovery splash + /// List of new guild features + /// New preferred locale + /// New updates channel ID + /// New rules channel ID + /// New system channel flags + /// Modify reason + /// + public async Task ModifyGuildAsync(ulong guild_id, Optional name, + Optional region, Optional verification_level, + Optional default_message_notifications, Optional mfa_level, + Optional explicit_content_filter, Optional afk_channel_id, + Optional afk_timeout, Optional iconb64, Optional owner_id, Optional splashb64, + Optional systemChannelId, Optional banner, Optional description, + Optional discorverySplash, Optional> features, Optional preferredLocale, + Optional publicUpdatesChannelId, Optional rulesChannelId, Optional systemChannelFlags, + string reason) + => await this.ApiClient.ModifyGuildAsync(guild_id, name, region, verification_level, default_message_notifications, mfa_level, explicit_content_filter, afk_channel_id, afk_timeout, iconb64, + owner_id, splashb64, systemChannelId, banner, description, discorverySplash, features, preferredLocale, publicUpdatesChannelId, rulesChannelId, systemChannelFlags, reason); + + /// + /// Modifies a guild + /// + /// Guild ID + /// Guild modifications + /// + public async Task ModifyGuildAsync(ulong guild_id, Action action) + { + var mdl = new GuildEditModel(); + action(mdl); + + if (mdl.AfkChannel.HasValue) + if (mdl.AfkChannel.Value.Type != ChannelType.Voice) + throw new ArgumentException("AFK channel needs to be a voice channel!"); + + var iconb64 = Optional.FromNoValue(); + if (mdl.Icon.HasValue && mdl.Icon.Value != null) + using (var imgtool = new ImageTool(mdl.Icon.Value)) + iconb64 = imgtool.GetBase64(); + else if (mdl.Icon.HasValue) + iconb64 = null; + + var splashb64 = Optional.FromNoValue(); + if (mdl.Splash.HasValue && mdl.Splash.Value != null) + using (var imgtool = new ImageTool(mdl.Splash.Value)) + splashb64 = imgtool.GetBase64(); + else if (mdl.Splash.HasValue) + splashb64 = null; + + var bannerb64 = Optional.FromNoValue(); + + if (mdl.Banner.HasValue && mdl.Banner.Value != null) + using (var imgtool = new ImageTool(mdl.Banner.Value)) + bannerb64 = imgtool.GetBase64(); + else if (mdl.Banner.HasValue) + bannerb64 = null; + + return await this.ApiClient.ModifyGuildAsync(guild_id, mdl.Name, mdl.Region.IfPresent(x => x.Id), mdl.VerificationLevel, mdl.DefaultMessageNotifications, + mdl.MfaLevel, mdl.ExplicitContentFilter, mdl.AfkChannel.IfPresent(x => x?.Id), mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(x => x.Id), + splashb64, mdl.SystemChannel.IfPresent(x => x?.Id), bannerb64, mdl.Description, mdl.DiscoverySplash, mdl.Features, mdl.PreferredLocale, + mdl.PublicUpdatesChannel.IfPresent(e => e?.Id), mdl.RulesChannel.IfPresent(e => e?.Id), mdl.SystemChannelFlags, mdl.AuditLogReason); + } - /// - /// Gets guild bans. - /// - /// The ID of the guild to get the bans from. - /// The number of users to return (up to maximum 1000, default 1000). - /// Consider only users before the given user ID. - /// Consider only users after the given user ID. - /// A collection of the guild's bans. - public Task> GetGuildBansAsync(ulong guild_id, int? limit = null, ulong? before = null, ulong? after = null) - => this.ApiClient.GetGuildBansAsync(guild_id, limit, before, after); - - /// - /// Gets the ban of the specified user. Requires Ban Members permission. - /// - /// The ID of the guild to get the ban from. - /// The ID of the user to get the ban for. - /// A guild ban object. - public Task GetGuildBanAsync(ulong guild_id, ulong user_id) - => this.ApiClient.GetGuildBanAsync(guild_id, user_id); - - /// - /// Creates guild ban - /// - /// Guild ID - /// User ID - /// Days to delete messages - /// Reason why this member was banned - /// - public Task CreateGuildBanAsync(ulong guild_id, ulong user_id, int delete_message_days, string reason) - => this.ApiClient.CreateGuildBanAsync(guild_id, user_id, delete_message_days, reason); - - /// - /// Removes a guild ban - /// - /// Guild ID - /// User to unban - /// Reason why this member was unbanned - /// - public Task RemoveGuildBanAsync(ulong guild_id, ulong user_id, string reason) - => this.ApiClient.RemoveGuildBanAsync(guild_id, user_id, reason); - - /// - /// Leaves a guild - /// - /// Guild ID - /// - public Task LeaveGuildAsync(ulong guild_id) - => this.ApiClient.LeaveGuildAsync(guild_id); - - /// - /// Adds a member to a guild - /// - /// Guild ID - /// User ID - /// Access token - /// User nickname - /// User roles - /// Whether this user should be muted on join - /// Whether this user should be deafened on join - /// - public Task AddGuildMemberAsync(ulong guild_id, ulong user_id, string access_token, string nick, IEnumerable roles, bool muted, bool deafened) - => this.ApiClient.AddGuildMemberAsync(guild_id, user_id, access_token, nick, roles, muted, deafened); - - /// - /// Gets all guild members - /// - /// Guild ID - /// Member download limit - /// Gets members after this ID - /// - public async Task> ListGuildMembersAsync(ulong guild_id, int? limit, ulong? after) + /// + /// Gets guild bans. + /// + /// The ID of the guild to get the bans from. + /// The number of users to return (up to maximum 1000, default 1000). + /// Consider only users before the given user ID. + /// Consider only users after the given user ID. + /// A collection of the guild's bans. + public async Task> GetGuildBansAsync(ulong guild_id, int? limit = null, ulong? before = null, ulong? after = null) + => await this.ApiClient.GetGuildBansAsync(guild_id, limit, before, after); + + /// + /// Gets the ban of the specified user. Requires Ban Members permission. + /// + /// The ID of the guild to get the ban from. + /// The ID of the user to get the ban for. + /// A guild ban object. + public async Task GetGuildBanAsync(ulong guild_id, ulong user_id) + => await this.ApiClient.GetGuildBanAsync(guild_id, user_id); + + /// + /// Creates guild ban + /// + /// Guild ID + /// User ID + /// Days to delete messages + /// Reason why this member was banned + /// + public async Task CreateGuildBanAsync(ulong guild_id, ulong user_id, int delete_message_days, string reason) + => await this.ApiClient.CreateGuildBanAsync(guild_id, user_id, delete_message_days, reason); + + /// + /// Removes a guild ban + /// + /// Guild ID + /// User to unban + /// Reason why this member was unbanned + /// + public async Task RemoveGuildBanAsync(ulong guild_id, ulong user_id, string reason) + => await this.ApiClient.RemoveGuildBanAsync(guild_id, user_id, reason); + + /// + /// Leaves a guild + /// + /// Guild ID + /// + public async Task LeaveGuildAsync(ulong guild_id) + => await this.ApiClient.LeaveGuildAsync(guild_id); + + /// + /// Adds a member to a guild + /// + /// Guild ID + /// User ID + /// Access token + /// User nickname + /// User roles + /// Whether this user should be muted on join + /// Whether this user should be deafened on join + /// + public async Task AddGuildMemberAsync(ulong guild_id, ulong user_id, string access_token, string nick, IEnumerable roles, bool muted, bool deafened) + => await this.ApiClient.AddGuildMemberAsync(guild_id, user_id, access_token, muted, deafened, nick, roles); + + /// + /// Gets all guild members + /// + /// Guild ID + /// Member download limit + /// Gets members after this ID + /// + public async Task> ListGuildMembersAsync(ulong guild_id, int? limit, ulong? after) + { + var recmbr = new List(); + + var recd = limit ?? 1000; + var lim = limit ?? 1000; + var last = after; + while (recd == lim) { - var recmbr = new List(); + var tms = await this.ApiClient.ListGuildMembersAsync(guild_id, lim, last == 0 ? null : (ulong?)last); + recd = tms.Count; - var recd = limit ?? 1000; - var lim = limit ?? 1000; - var last = after; - while (recd == lim) + foreach (var xtm in tms) { - var tms = await this.ApiClient.ListGuildMembersAsync(guild_id, lim, last == 0 ? null : (ulong?)last); - recd = tms.Count; - - foreach (var xtm in tms) - { - last = xtm.User.Id; + last = xtm.User.Id; - if (this.UserCache.ContainsKey(xtm.User.Id)) - continue; + if (this.UserCache.ContainsKey(xtm.User.Id)) + continue; - var usr = new DiscordUser(xtm.User) { Discord = this }; - this.UpdateUserCache(usr); - } - - recmbr.AddRange(tms.Select(xtm => new DiscordMember(xtm) { Discord = this, _guild_id = guild_id })); + var usr = new DiscordUser(xtm.User) { Discord = this }; + this.UpdateUserCache(usr); } - return new ReadOnlyCollection(recmbr); + recmbr.AddRange(tms.Select(xtm => new DiscordMember(xtm) { Discord = this, _guild_id = guild_id })); } - /// - /// Add role to guild member - /// - /// Guild ID - /// User ID - /// Role ID - /// Reason this role gets added - /// - public Task AddGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) - => this.ApiClient.AddGuildMemberRoleAsync(guild_id, user_id, role_id, reason); - - /// - /// Remove role from member - /// - /// Guild ID - /// User ID - /// Role ID - /// Reason this role gets removed - /// - public Task RemoveGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) - => this.ApiClient.RemoveGuildMemberRoleAsync(guild_id, user_id, role_id, reason); - - /// - /// Updates a role's position - /// - /// Guild ID - /// Role ID - /// Role position - /// Reason this position was modified - /// - public Task UpdateRolePositionAsync(ulong guild_id, ulong role_id, int position, string reason = null) - { - var rgrrps = new List() - { - new RestGuildRoleReorderPayload { RoleId = role_id } - }; - return this.ApiClient.ModifyGuildRolePositionAsync(guild_id, rgrrps, reason); - } + return new ReadOnlyCollection(recmbr); + } - /// - /// Updates a channel's position - /// - /// Guild ID - /// Channel ID - /// Channel position - /// Reason this position was modified - /// Whether to sync channel permissions with the parent, if moving to a new category. - /// The new parent id if the channel is to be moved to a new category. - /// - public Task UpdateChannelPositionAsync(ulong guild_id, ulong channel_id, int position, string reason, bool? lockPermissions = null, ulong? parentId = null) + /// + /// Add role to guild member + /// + /// Guild ID + /// User ID + /// Role ID + /// Reason this role gets added + /// + public async Task AddGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) + => await this.ApiClient.AddGuildMemberRoleAsync(guild_id, user_id, role_id, reason); + + /// + /// Remove role from member + /// + /// Guild ID + /// User ID + /// Role ID + /// Reason this role gets removed + /// + public async Task RemoveGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) + => await this.ApiClient.RemoveGuildMemberRoleAsync(guild_id, user_id, role_id, reason); + + /// + /// Updates a role's position + /// + /// Guild ID + /// Role ID + /// Role position + /// Reason this position was modified + /// + public async Task UpdateRolePositionAsync(ulong guild_id, ulong role_id, int position, string reason = null) + { + var rgrrps = new List() { - var rgcrps = new List() - { - new RestGuildChannelReorderPayload { ChannelId = channel_id, Position = position, LockPermissions = lockPermissions, ParentId = parentId } - }; - return this.ApiClient.ModifyGuildChannelPositionAsync(guild_id, rgcrps, reason); - } + new RestGuildRoleReorderPayload { RoleId = role_id } + }; + await this.ApiClient.ModifyGuildRolePositionsAsync(guild_id, rgrrps, reason); + } - /// - /// Gets a guild's widget - /// - /// Guild ID - /// - public Task GetGuildWidgetAsync(ulong guild_id) - => this.ApiClient.GetGuildWidgetAsync(guild_id); - - /// - /// Gets a guild's widget settings - /// - /// Guild ID - /// - public Task GetGuildWidgetSettingsAsync(ulong guild_id) - => this.ApiClient.GetGuildWidgetSettingsAsync(guild_id); - - /// - /// Modifies a guild's widget settings - /// - /// Guild ID - /// If the widget is enabled or not - /// Widget channel ID - /// Reason the widget settings were modified - /// - public Task ModifyGuildWidgetSettingsAsync(ulong guild_id, bool? enabled = null, ulong? channel_id = null, string reason = null) - => this.ApiClient.ModifyGuildWidgetSettingsAsync(guild_id, enabled, channel_id, reason); - - /// - /// Gets a guild's membership screening form. - /// - /// Guild ID - /// The guild's membership screening form. - public Task GetGuildMembershipScreeningFormAsync(ulong guild_id) - => this.ApiClient.GetGuildMembershipScreeningFormAsync(guild_id); - - /// - /// Modifies a guild's membership screening form. - /// - /// Guild ID - /// Action to perform - /// The modified screening form. - public async Task ModifyGuildMembershipScreeningFormAsync(ulong guild_id, Action action) + /// + /// Updates a channel's position + /// + /// Guild ID + /// Channel ID + /// Channel position + /// Reason this position was modified + /// Whether to sync channel permissions with the parent, if moving to a new category. + /// The new parent id if the channel is to be moved to a new category. + /// + public async Task UpdateChannelPositionAsync(ulong guild_id, ulong channel_id, int position, string reason, bool? lockPermissions = null, ulong? parentId = null) + { + var rgcrps = new List() { - var mdl = new MembershipScreeningEditModel(); - action(mdl); - return await this.ApiClient.ModifyGuildMembershipScreeningFormAsync(guild_id, mdl.Enabled, mdl.Fields, mdl.Description); - } + new RestGuildChannelReorderPayload { ChannelId = channel_id, Position = position, LockPermissions = lockPermissions, ParentId = parentId } + }; + await this.ApiClient.ModifyGuildChannelPositionAsync(guild_id, rgcrps, reason); + } - /// - /// Gets a guild's vanity url - /// - /// The ID of the guild. - /// The guild's vanity url. - public Task GetGuildVanityUrlAsync(ulong guildId) - => this.ApiClient.GetGuildVanityUrlAsync(guildId); - - /// - /// Updates the current user's suppress state in a stage channel. - /// - /// The ID of the guild. - /// The ID of the channel. - /// Toggles the suppress state. - /// Sets the time the user requested to speak. - public Task UpdateCurrentUserVoiceStateAsync(ulong guildId, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) - => this.ApiClient.UpdateCurrentUserVoiceStateAsync(guildId, channelId, suppress, requestToSpeakTimestamp); - - /// - /// Updates a member's suppress state in a stage channel. - /// - /// The ID of the guild. - /// The ID of the member. - /// The ID of the stage channel. - /// Toggles the member's suppress state. - /// - public Task UpdateUserVoiceStateAsync(ulong guildId, ulong userId, ulong channelId, bool? suppress) - => this.ApiClient.UpdateUserVoiceStateAsync(guildId, userId, channelId, suppress); - #endregion - - #region Channel - /// - /// Creates a guild channel - /// - /// Channel ID - /// Channel name - /// Channel type - /// Channel parent ID - /// Channel topic - /// Voice channel bitrate - /// Voice channel user limit - /// Channel overwrites - /// Whether this channel should be marked as NSFW - /// Slow mode timeout for users. - /// Voice channel video quality mode. - /// Sorting position of the channel. - /// Reason this channel was created - /// Default duration for newly created forum posts in the channel. - /// Default emoji used for reacting to forum posts. - /// Tags available for use by forum posts in the channel. - /// Default sorting order for forum posts in the channel. - /// - public Task CreateGuildChannelAsync + /// + /// Gets a guild's widget + /// + /// Guild ID + /// + public async Task GetGuildWidgetAsync(ulong guild_id) + => await this.ApiClient.GetGuildWidgetAsync(guild_id); + + /// + /// Gets a guild's widget settings + /// + /// Guild ID + /// + public async Task GetGuildWidgetSettingsAsync(ulong guild_id) + => await this.ApiClient.GetGuildWidgetSettingsAsync(guild_id); + + /// + /// Modifies a guild's widget settings + /// + /// Guild ID + /// If the widget is enabled or not + /// Widget channel ID + /// Reason the widget settings were modified + /// + public async Task ModifyGuildWidgetSettingsAsync(ulong guild_id, bool? enabled = null, ulong? channel_id = null, string reason = null) + => await this.ApiClient.ModifyGuildWidgetSettingsAsync(guild_id, enabled, channel_id, reason); + + /// + /// Gets a guild's membership screening form. + /// + /// Guild ID + /// The guild's membership screening form. + public async Task GetGuildMembershipScreeningFormAsync(ulong guild_id) + => await this.ApiClient.GetGuildMembershipScreeningFormAsync(guild_id); + + /// + /// Modifies a guild's membership screening form. + /// + /// Guild ID + /// Action to perform + /// The modified screening form. + public async Task ModifyGuildMembershipScreeningFormAsync(ulong guild_id, Action action) + { + var mdl = new MembershipScreeningEditModel(); + action(mdl); + return await this.ApiClient.ModifyGuildMembershipScreeningFormAsync(guild_id, mdl.Enabled, mdl.Fields, mdl.Description); + } + + /// + /// Gets a guild's vanity url + /// + /// The ID of the guild. + /// The guild's vanity url. + public async Task GetGuildVanityUrlAsync(ulong guildId) + => await this.ApiClient.GetGuildVanityUrlAsync(guildId); + + /// + /// Updates the current user's suppress state in a stage channel. + /// + /// The ID of the guild. + /// The ID of the channel. + /// Toggles the suppress state. + /// Sets the time the user requested to speak. + public async Task UpdateCurrentUserVoiceStateAsync(ulong guildId, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) + => await this.ApiClient.UpdateCurrentUserVoiceStateAsync(guildId, channelId, suppress, requestToSpeakTimestamp); + + /// + /// Updates a member's suppress state in a stage channel. + /// + /// The ID of the guild. + /// The ID of the member. + /// The ID of the stage channel. + /// Toggles the member's suppress state. + /// + public async Task UpdateUserVoiceStateAsync(ulong guildId, ulong userId, ulong channelId, bool? suppress) + => await this.ApiClient.UpdateUserVoiceStateAsync(guildId, userId, channelId, suppress); + #endregion + + #region Channel + /// + /// Creates a guild channel + /// + /// Channel ID + /// Channel name + /// Channel type + /// Channel parent ID + /// Channel topic + /// Voice channel bitrate + /// Voice channel user limit + /// Channel overwrites + /// Whether this channel should be marked as NSFW + /// Slow mode timeout for users. + /// Voice channel video quality mode. + /// Sorting position of the channel. + /// Reason this channel was created + /// Default duration for newly created forum posts in the channel. + /// Default emoji used for reacting to forum posts. + /// Tags available for use by forum posts in the channel. + /// Default sorting order for forum posts in the channel. + /// + public async Task CreateGuildChannelAsync + ( + ulong id, + string name, + ChannelType type, + ulong? parent, + Optional topic, + int? bitrate, + int? userLimit, + IEnumerable overwrites, + bool? nsfw, + Optional perUserRateLimit, + VideoQualityMode? qualityMode, + int? position, + string reason, + AutoArchiveDuration? defaultAutoArchiveDuration = null, + DefaultReaction? defaultReactionEmoji = null, + IEnumerable availableTags = null, + DefaultSortOrder? defaultSortOrder = null + ) + { + if (type is not (ChannelType.Text or ChannelType.Voice or ChannelType.Category or ChannelType.News or ChannelType.Stage or ChannelType.GuildForum)) + throw new ArgumentException("Channel type must be text, voice, stage, category, or a forum.", nameof(type)); + + return await this.ApiClient.CreateGuildChannelAsync ( - ulong id, - string name, - ChannelType type, - ulong? parent, - Optional topic, - int? bitrate, - int? userLimit, - IEnumerable overwrites, - bool? nsfw, - Optional perUserRateLimit, - VideoQualityMode? qualityMode, - int? position, - string reason, - AutoArchiveDuration? defaultAutoArchiveDuration = null, - DefaultReaction? defaultReactionEmoji = null, - IEnumerable availableTags = null, - DefaultSortOrder? defaultSortOrder = null - ) - { - if (type is not (ChannelType.Text or ChannelType.Voice or ChannelType.Category or ChannelType.News or ChannelType.Stage or ChannelType.GuildForum)) - throw new ArgumentException("Channel type must be text, voice, stage, category, or a forum.", nameof(type)); - - return this.ApiClient.CreateGuildChannelAsync - ( - id, - name, - type, - parent, - topic, - bitrate, - userLimit, - overwrites, - nsfw, - perUserRateLimit, - qualityMode, - position, - reason, - defaultAutoArchiveDuration, - defaultReactionEmoji, - availableTags, - defaultSortOrder - ); - } + id, + name, + type, + parent, + topic, + bitrate, + userLimit, + overwrites, + nsfw, + perUserRateLimit, + qualityMode, + position, + reason, + defaultAutoArchiveDuration, + defaultReactionEmoji, + availableTags, + defaultSortOrder + ); + } - /// - /// Modifies a channel - /// - /// Channel ID - /// New channel name - /// New channel position - /// New channel topic - /// Whether this channel should be marked as NSFW - /// New channel parent - /// New voice channel bitrate - /// New voice channel user limit - /// Slow mode timeout for users. - /// New region override. - /// New video quality mode. - /// New channel type. - /// New channel permission overwrites. - /// Reason why this channel was modified - /// Channel flags. - /// Default duration for newly created forum posts in the channel. - /// Default emoji used for reacting to forum posts. - /// Tags available for use by forum posts in the channel. - /// Default per-user ratelimit for forum posts in the channel. - /// Default sorting order for forum posts in the channel. - /// Default layout for forum posts in the channel. - /// - public Task ModifyChannelAsync + /// + /// Modifies a channel + /// + /// Channel ID + /// New channel name + /// New channel position + /// New channel topic + /// Whether this channel should be marked as NSFW + /// New channel parent + /// New voice channel bitrate + /// New voice channel user limit + /// Slow mode timeout for users. + /// New region override. + /// New video quality mode. + /// New channel type. + /// New channel permission overwrites. + /// Reason why this channel was modified + /// Channel flags. + /// Default duration for newly created forum posts in the channel. + /// Default emoji used for reacting to forum posts. + /// Tags available for use by forum posts in the channel. + /// Default per-user ratelimit for forum posts in the channel. + /// Default sorting order for forum posts in the channel. + /// Default layout for forum posts in the channel. + /// + public async Task ModifyChannelAsync + ( + ulong id, + string name, + int? position, + Optional topic, + bool? nsfw, + Optional parent, + int? bitrate, + int? userLimit, + Optional perUserRateLimit, + Optional rtcRegion, + VideoQualityMode? qualityMode, + Optional type, + IEnumerable permissionOverwrites, + string reason, + Optional flags, + IEnumerable? availableTags, + Optional defaultAutoArchiveDuration, + Optional defaultReactionEmoji, + Optional defaultPerUserRatelimit, + Optional defaultSortOrder, + Optional defaultForumLayout + ) + => await this.ApiClient.ModifyChannelAsync ( - ulong id, - string name, - int? position, - Optional topic, - bool? nsfw, - Optional parent, - int? bitrate, - int? userLimit, - Optional perUserRateLimit, - Optional rtcRegion, - VideoQualityMode? qualityMode, - Optional type, - IEnumerable permissionOverwrites, - string reason, - Optional flags, - IEnumerable? availableTags, - Optional defaultAutoArchiveDuration, - Optional defaultReactionEmoji, - Optional defaultPerUserRatelimit, - Optional defaultSortOrder, - Optional defaultForumLayout - ) - => this.ApiClient.ModifyChannelAsync - ( - id, - name, - position, - topic, - nsfw, - parent, - bitrate, - userLimit, - perUserRateLimit, - rtcRegion.IfPresent(e => e?.Id), - qualityMode, - type, - permissionOverwrites, - reason, - flags, - availableTags, - defaultAutoArchiveDuration, - defaultReactionEmoji, - defaultPerUserRatelimit, - defaultSortOrder, - defaultForumLayout - ); - - /// - /// Modifies a channel - /// - /// Channel ID - /// Channel modifications - /// - public Task ModifyChannelAsync(ulong channelId, Action action) - { - var mdl = new ChannelEditModel(); - action(mdl); - - return this.ApiClient.ModifyChannelAsync - ( - channelId, mdl.Name, - mdl.Position, - mdl.Topic, - mdl.Nsfw, - mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), - mdl.Bitrate, - mdl.Userlimit, - mdl.PerUserRateLimit, - mdl.RtcRegion.IfPresent(r => r?.Id), - mdl.QualityMode, - mdl.Type, - mdl.PermissionOverwrites, - mdl.AuditLogReason, - mdl.Flags, - mdl.AvailableTags, - mdl.DefaultAutoArchiveDuration, - mdl.DefaultReaction, - mdl.DefaultThreadRateLimit, - mdl.DefaultSortOrder, - mdl.DefaultForumLayout - ); - } + id, + name, + position, + topic, + nsfw, + parent, + bitrate, + userLimit, + perUserRateLimit, + rtcRegion.IfPresent(e => e?.Id), + qualityMode, + type, + permissionOverwrites, + flags, + availableTags, + defaultAutoArchiveDuration, + defaultReactionEmoji, + defaultPerUserRatelimit, + defaultSortOrder, + defaultForumLayout, + reason + ); + + /// + /// Modifies a channel + /// + /// Channel ID + /// Channel modifications + /// + public async Task ModifyChannelAsync(ulong channelId, Action action) + { + var mdl = new ChannelEditModel(); + action(mdl); - /// - /// Gets a channel object - /// - /// Channel ID - /// - public Task GetChannelAsync(ulong id) - => this.ApiClient.GetChannelAsync(id); - - /// - /// Deletes a channel - /// - /// Channel ID - /// Reason why this channel was deleted - /// - public Task DeleteChannelAsync(ulong id, string reason) - => this.ApiClient.DeleteChannelAsync(id, reason); - - /// - /// Gets message in a channel - /// - /// Channel ID - /// Message ID - /// - public Task GetMessageAsync(ulong channel_id, ulong message_id) - => this.ApiClient.GetMessageAsync(channel_id, message_id); - - /// - /// Sends a message - /// - /// Channel ID - /// Message (text) content - /// - public Task CreateMessageAsync(ulong channel_id, string content) - => this.ApiClient.CreateMessageAsync(channel_id, content, null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel ID - /// Embed to attach - /// - public Task CreateMessageAsync(ulong channel_id, DiscordEmbed embed) - => this.ApiClient.CreateMessageAsync(channel_id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel ID - /// Message (text) content - /// Embed to attach - /// - public Task CreateMessageAsync(ulong channel_id, string content, DiscordEmbed embed) - => this.ApiClient.CreateMessageAsync(channel_id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel ID - /// The Discord Message builder. - /// - public Task CreateMessageAsync(ulong channel_id, DiscordMessageBuilder builder) - => this.ApiClient.CreateMessageAsync(channel_id, builder); - - /// - /// Sends a message - /// - /// Channel ID - /// The Discord Message builder. - /// - public Task CreateMessageAsync(ulong channel_id, Action action) - { - var builder = new DiscordMessageBuilder(); - action(builder); - return this.ApiClient.CreateMessageAsync(channel_id, builder); - } + await this.ApiClient.ModifyChannelAsync + ( + channelId, mdl.Name, + mdl.Position, + mdl.Topic, + mdl.Nsfw, + mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), + mdl.Bitrate, + mdl.Userlimit, + mdl.PerUserRateLimit, + mdl.RtcRegion.IfPresent(r => r?.Id), + mdl.QualityMode, + mdl.Type, + mdl.PermissionOverwrites, + mdl.Flags, + mdl.AvailableTags, + mdl.DefaultAutoArchiveDuration, + mdl.DefaultReaction, + mdl.DefaultThreadRateLimit, + mdl.DefaultSortOrder, + mdl.DefaultForumLayout, + mdl.AuditLogReason + ); + } - /// - /// Gets channels from a guild - /// - /// Guild ID - /// - public Task> GetGuildChannelsAsync(ulong guild_id) - => this.ApiClient.GetGuildChannelsAsync(guild_id); - - /// - /// Gets messages from a channel - /// - /// Channel ID - /// Limit of messages to get - /// Gets messages before this ID - /// Gets messages after this ID - /// Gets messages around this ID - /// - public Task> GetChannelMessagesAsync(ulong channel_id, int limit, ulong? before, ulong? after, ulong? around) - => this.ApiClient.GetChannelMessagesAsync(channel_id, limit, before, after, around); - - /// - /// Gets a message from a channel - /// - /// Channel ID - /// Message ID - /// - public Task GetChannelMessageAsync(ulong channel_id, ulong message_id) - => this.ApiClient.GetChannelMessageAsync(channel_id, message_id); - - /// - /// Edits a message - /// - /// Channel ID - /// Message ID - /// New message content - /// - public Task EditMessageAsync(ulong channel_id, ulong message_id, Optional content) - => this.ApiClient.EditMessageAsync(channel_id, message_id, content, default, default, default, Array.Empty(), null, default); - - /// - /// Edits a message - /// - /// Channel ID - /// Message ID - /// New message embed - /// - public Task EditMessageAsync(ulong channel_id, ulong message_id, Optional embed) - => this.ApiClient.EditMessageAsync(channel_id, message_id, default, embed.HasValue ? new[] { embed.Value } : Array.Empty(), default, default, Array.Empty(), null, default); - - /// - /// Edits a message - /// - /// Channel ID - /// Message ID - /// The builder of the message to edit. - /// Whether to suppress embeds on the message. - /// Attached files to keep. - /// - public async Task EditMessageAsync(ulong channel_id, ulong message_id, DiscordMessageBuilder builder, bool suppressEmbeds = false, IEnumerable attachments = default) - { - builder.Validate(true); + /// + /// Gets a channel object + /// + /// Channel ID + /// + public async Task GetChannelAsync(ulong id) + => await this.ApiClient.GetChannelAsync(id); + + /// + /// Deletes a channel + /// + /// Channel ID + /// Reason why this channel was deleted + /// + public async Task DeleteChannelAsync(ulong id, string reason) + => await this.ApiClient.DeleteChannelAsync(id, reason); + + /// + /// Gets message in a channel + /// + /// Channel ID + /// Message ID + /// + public async Task GetMessageAsync(ulong channel_id, ulong message_id) + => await this.ApiClient.GetMessageAsync(channel_id, message_id); + + /// + /// Sends a message + /// + /// Channel ID + /// Message (text) content + /// + public async Task CreateMessageAsync(ulong channel_id, string content) + => await this.ApiClient.CreateMessageAsync(channel_id, content, null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel ID + /// Embed to attach + /// + public async Task CreateMessageAsync(ulong channel_id, DiscordEmbed embed) + => await this.ApiClient.CreateMessageAsync(channel_id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel ID + /// Message (text) content + /// Embed to attach + /// + public async Task CreateMessageAsync(ulong channel_id, string content, DiscordEmbed embed) + => await this.ApiClient.CreateMessageAsync(channel_id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel ID + /// The Discord Message builder. + /// + public async Task CreateMessageAsync(ulong channel_id, DiscordMessageBuilder builder) + => await this.ApiClient.CreateMessageAsync(channel_id, builder); + + /// + /// Sends a message + /// + /// Channel ID + /// The Discord Message builder. + /// + public async Task CreateMessageAsync(ulong channel_id, Action action) + { + var builder = new DiscordMessageBuilder(); + action(builder); + return await this.ApiClient.CreateMessageAsync(channel_id, builder); + } - return await this.ApiClient.EditMessageAsync(channel_id, message_id, builder.Content, new Optional>(builder.Embeds), builder._mentions, builder.Components, builder.Files, suppressEmbeds ? MessageFlags.SuppressedEmbeds : null, attachments); - } + /// + /// Gets channels from a guild + /// + /// Guild ID + /// + public async Task> GetGuildChannelsAsync(ulong guild_id) + => await this.ApiClient.GetGuildChannelsAsync(guild_id); + + /// + /// Gets messages from a channel + /// + /// Channel ID + /// Limit of messages to get + /// Gets messages before this ID + /// Gets messages after this ID + /// Gets messages around this ID + /// + public async Task> GetChannelMessagesAsync(ulong channel_id, int limit, ulong? before, ulong? after, ulong? around) + => await this.ApiClient.GetChannelMessagesAsync(channel_id, limit, before, after, around); + + /// + /// Gets a message from a channel + /// + /// Channel ID + /// Message ID + /// + public async Task GetChannelMessageAsync(ulong channel_id, ulong message_id) + => await this.ApiClient.GetChannelMessageAsync(channel_id, message_id); + + /// + /// Edits a message + /// + /// Channel ID + /// Message ID + /// New message content + /// + public async Task EditMessageAsync(ulong channel_id, ulong message_id, Optional content) + => await this.ApiClient.EditMessageAsync(channel_id, message_id, content, default, default, default, Array.Empty(), null, default); + + /// + /// Edits a message + /// + /// Channel ID + /// Message ID + /// New message embed + /// + public async Task EditMessageAsync(ulong channel_id, ulong message_id, Optional embed) + => await this.ApiClient.EditMessageAsync(channel_id, message_id, default, embed.HasValue ? new[] { embed.Value } : Array.Empty(), default, default, Array.Empty(), null, default); + + /// + /// Edits a message + /// + /// Channel ID + /// Message ID + /// The builder of the message to edit. + /// Whether to suppress embeds on the message. + /// Attached files to keep. + /// + public async Task EditMessageAsync(ulong channel_id, ulong message_id, DiscordMessageBuilder builder, bool suppressEmbeds = false, IEnumerable attachments = default) + { + builder.Validate(true); - /// - /// Modifies the visibility of embeds in a message. - /// - /// Channel ID - /// Message ID - /// Whether to hide all embeds. - public Task ModifyEmbedSuppressionAsync(ulong channel_id, ulong message_id, bool hideEmbeds) - => this.ApiClient.EditMessageAsync(channel_id, message_id, default, default, default, default, Array.Empty(), hideEmbeds ? MessageFlags.SuppressedEmbeds : null, default); - - /// - /// Deletes a message - /// - /// Channel ID - /// Message ID - /// Why this message was deleted - /// - public Task DeleteMessageAsync(ulong channel_id, ulong message_id, string reason) - => this.ApiClient.DeleteMessageAsync(channel_id, message_id, reason); - - /// - /// Deletes multiple messages - /// - /// Channel ID - /// Message IDs - /// Reason these messages were deleted - /// - public Task DeleteMessagesAsync(ulong channel_id, IEnumerable message_ids, string reason) - => this.ApiClient.DeleteMessagesAsync(channel_id, message_ids, reason); - - /// - /// Gets a channel's invites - /// - /// Channel ID - /// - public Task> GetChannelInvitesAsync(ulong channel_id) - => this.ApiClient.GetChannelInvitesAsync(channel_id); - - /// - /// Creates a channel invite - /// - /// Channel ID - /// For how long the invite should exist - /// How often the invite may be used - /// Whether this invite should be temporary - /// Whether this invite should be unique (false might return an existing invite) - /// Why you made an invite - /// The target type of the invite, for stream and embedded application invites. - /// The ID of the target user. - /// The ID of the target application. - /// - public Task CreateChannelInviteAsync(ulong channel_id, int max_age, int max_uses, bool temporary, bool unique, string reason, InviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) - => this.ApiClient.CreateChannelInviteAsync(channel_id, max_age, max_uses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); - - /// - /// Deletes channel overwrite - /// - /// Channel ID - /// Overwrite ID - /// Reason it was deleted - /// - public Task DeleteChannelPermissionAsync(ulong channel_id, ulong overwrite_id, string reason) - => this.ApiClient.DeleteChannelPermissionAsync(channel_id, overwrite_id, reason); - - /// - /// Edits channel overwrite - /// - /// Channel ID - /// Overwrite ID - /// Permissions to allow - /// Permissions to deny - /// Overwrite type - /// Reason this overwrite was created - /// - public Task EditChannelPermissionsAsync(ulong channel_id, ulong overwrite_id, Permissions allow, Permissions deny, string type, string reason) - => this.ApiClient.EditChannelPermissionsAsync(channel_id, overwrite_id, allow, deny, type, reason); - - /// - /// Send a typing indicator to a channel - /// - /// Channel ID - /// - public Task TriggerTypingAsync(ulong channel_id) - => this.ApiClient.TriggerTypingAsync(channel_id); - - /// - /// Gets pinned messages - /// - /// Channel ID - /// - public Task> GetPinnedMessagesAsync(ulong channel_id) - => this.ApiClient.GetPinnedMessagesAsync(channel_id); - - /// - /// Unpins a message - /// - /// Channel ID - /// Message ID - /// - public Task UnpinMessageAsync(ulong channel_id, ulong message_id) - => this.ApiClient.UnpinMessageAsync(channel_id, message_id); - - /// - /// Joins a group DM - /// - /// Channel ID - /// DM nickname - /// - public Task JoinGroupDmAsync(ulong channel_id, string nickname) - => this.ApiClient.AddGroupDmRecipientAsync(channel_id, this.CurrentUser.Id, this.Configuration.Token, nickname); - - /// - /// Adds a member to a group DM - /// - /// Channel ID - /// User ID - /// User's access token - /// Nickname for user - /// - public Task GroupDmAddRecipientAsync(ulong channel_id, ulong user_id, string access_token, string nickname) - => this.ApiClient.AddGroupDmRecipientAsync(channel_id, user_id, access_token, nickname); - - /// - /// Leaves a group DM - /// - /// Channel ID - /// - public Task LeaveGroupDmAsync(ulong channel_id) - => this.ApiClient.RemoveGroupDmRecipientAsync(channel_id, this.CurrentUser.Id); - - /// - /// Removes a member from a group DM - /// - /// Channel ID - /// User ID - /// - public Task GroupDmRemoveRecipientAsync(ulong channel_id, ulong user_id) - => this.ApiClient.RemoveGroupDmRecipientAsync(channel_id, user_id); - - /// - /// Creates a group DM - /// - /// Access tokens - /// Nicknames per user - /// - public Task CreateGroupDmAsync(IEnumerable access_tokens, IDictionary nicks) - => this.ApiClient.CreateGroupDmAsync(access_tokens, nicks); - - /// - /// Creates a group DM with current user - /// - /// Access tokens - /// Nicknames - /// - public Task CreateGroupDmWithCurrentUserAsync(IEnumerable access_tokens, IDictionary nicks) - { - var a = access_tokens.ToList(); - a.Add(this.Configuration.Token); - return this.ApiClient.CreateGroupDmAsync(a, nicks); - } + return await this.ApiClient.EditMessageAsync(channel_id, message_id, builder.Content, new Optional>(builder.Embeds), builder._mentions, builder.Components, builder.Files, suppressEmbeds ? MessageFlags.SuppressedEmbeds : null, attachments); + } - /// - /// Creates a DM - /// - /// Recipient user ID - /// - public Task CreateDmAsync(ulong recipient_id) - => this.ApiClient.CreateDmAsync(recipient_id); - - /// - /// Follows a news channel - /// - /// ID of the channel to follow - /// ID of the channel to crosspost messages to - /// Thrown when the current user doesn't have on the target channel - public Task FollowChannelAsync(ulong channel_id, ulong webhook_channel_id) - => this.ApiClient.FollowChannelAsync(channel_id, webhook_channel_id); - - /// - /// Publishes a message in a news channel to following channels - /// - /// ID of the news channel the message to crosspost belongs to - /// ID of the message to crosspost - /// - /// Thrown when the current user doesn't have and/or - /// - public Task CrosspostMessageAsync(ulong channel_id, ulong message_id) - => this.ApiClient.CrosspostMessageAsync(channel_id, message_id); - - /// - /// Creates a stage instance in a stage channel. - /// - /// The ID of the stage channel to create it in. - /// The topic of the stage instance. - /// The privacy level of the stage instance. - /// The reason the stage instance was created. - /// The created stage instance. - public Task CreateStageInstanceAsync(ulong channelId, string topic, PrivacyLevel? privacyLevel = null, string reason = null) - => this.ApiClient.CreateStageInstanceAsync(channelId, topic, privacyLevel, reason); - - /// - /// Gets a stage instance in a stage channel. - /// - /// The ID of the channel. - /// The stage instance in the channel. - public Task GetStageInstanceAsync(ulong channelId) - => this.ApiClient.GetStageInstanceAsync(channelId); - - /// - /// Modifies a stage instance in a stage channel. - /// - /// The ID of the channel to modify the stage instance of. - /// Action to perform. - /// The modified stage instance. - public async Task ModifyStageInstanceAsync(ulong channelId, Action action) - { - var mdl = new StageInstanceEditModel(); - action(mdl); - return await this.ApiClient.ModifyStageInstanceAsync(channelId, mdl.Topic, mdl.PrivacyLevel, mdl.AuditLogReason); - } + /// + /// Modifies the visibility of embeds in a message. + /// + /// Channel ID + /// Message ID + /// Whether to hide all embeds. + public async Task ModifyEmbedSuppressionAsync(ulong channel_id, ulong message_id, bool hideEmbeds) + => await this.ApiClient.EditMessageAsync(channel_id, message_id, default, default, default, default, Array.Empty(), hideEmbeds ? MessageFlags.SuppressedEmbeds : null, default); + + /// + /// Deletes a message + /// + /// Channel ID + /// Message ID + /// Why this message was deleted + /// + public async Task DeleteMessageAsync(ulong channel_id, ulong message_id, string reason) + => await this.ApiClient.DeleteMessageAsync(channel_id, message_id, reason); + + /// + /// Deletes multiple messages + /// + /// Channel ID + /// Message IDs + /// Reason these messages were deleted + /// + public async Task DeleteMessagesAsync(ulong channel_id, IEnumerable message_ids, string reason) + => await this.ApiClient.DeleteMessagesAsync(channel_id, message_ids, reason); + + /// + /// Gets a channel's invites + /// + /// Channel ID + /// + public async Task> GetChannelInvitesAsync(ulong channel_id) + => await this.ApiClient.GetChannelInvitesAsync(channel_id); + + /// + /// Creates a channel invite + /// + /// Channel ID + /// For how long the invite should exist + /// How often the invite may be used + /// Whether this invite should be temporary + /// Whether this invite should be unique (false might return an existing invite) + /// Why you made an invite + /// The target type of the invite, for stream and embedded application invites. + /// The ID of the target user. + /// The ID of the target application. + /// + public async Task CreateChannelInviteAsync(ulong channel_id, int max_age, int max_uses, bool temporary, bool unique, string reason, InviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) + => await this.ApiClient.CreateChannelInviteAsync(channel_id, max_age, max_uses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); + + /// + /// Deletes channel overwrite + /// + /// Channel ID + /// Overwrite ID + /// Reason it was deleted + /// + public async Task DeleteChannelPermissionAsync(ulong channel_id, ulong overwrite_id, string reason) + => await this.ApiClient.DeleteChannelPermissionAsync(channel_id, overwrite_id, reason); + + /// + /// Edits channel overwrite + /// + /// Channel ID + /// Overwrite ID + /// Permissions to allow + /// Permissions to deny + /// Overwrite type + /// Reason this overwrite was created + /// + public async Task EditChannelPermissionsAsync(ulong channel_id, ulong overwrite_id, Permissions allow, Permissions deny, string type, string reason) + => await this.ApiClient.EditChannelPermissionsAsync(channel_id, overwrite_id, allow, deny, type, reason); + + /// + /// Send a typing indicator to a channel + /// + /// Channel ID + /// + public async Task TriggerTypingAsync(ulong channel_id) + => await this.ApiClient.TriggerTypingAsync(channel_id); + + /// + /// Gets pinned messages + /// + /// Channel ID + /// + public async Task> GetPinnedMessagesAsync(ulong channel_id) + => await this.ApiClient.GetPinnedMessagesAsync(channel_id); + + /// + /// Unpins a message + /// + /// Channel ID + /// Message ID + /// + public async Task UnpinMessageAsync(ulong channel_id, ulong message_id) + => await this.ApiClient.UnpinMessageAsync(channel_id, message_id); + + /// + /// Joins a group DM + /// + /// Channel ID + /// DM nickname + /// + public async Task JoinGroupDmAsync(ulong channel_id, string nickname) + => await this.ApiClient.AddGroupDmRecipientAsync(channel_id, this.CurrentUser.Id, this.Configuration.Token, nickname); + + /// + /// Adds a member to a group DM + /// + /// Channel ID + /// User ID + /// User's access token + /// Nickname for user + /// + public async Task GroupDmAddRecipientAsync(ulong channel_id, ulong user_id, string access_token, string nickname) + => await this.ApiClient.AddGroupDmRecipientAsync(channel_id, user_id, access_token, nickname); + + /// + /// Leaves a group DM + /// + /// Channel ID + /// + public async Task LeaveGroupDmAsync(ulong channel_id) + => await this.ApiClient.RemoveGroupDmRecipientAsync(channel_id, this.CurrentUser.Id); + + /// + /// Removes a member from a group DM + /// + /// Channel ID + /// User ID + /// + public async Task GroupDmRemoveRecipientAsync(ulong channel_id, ulong user_id) + => await this.ApiClient.RemoveGroupDmRecipientAsync(channel_id, user_id); + + /// + /// Creates a group DM + /// + /// Access tokens + /// Nicknames per user + /// + public async Task CreateGroupDmAsync(IEnumerable access_tokens, IDictionary nicks) + => await this.ApiClient.CreateGroupDmAsync(access_tokens, nicks); + + /// + /// Creates a group DM with current user + /// + /// Access tokens + /// Nicknames + /// + public async Task CreateGroupDmWithCurrentUserAsync(IEnumerable access_tokens, IDictionary nicks) + { + var a = access_tokens.ToList(); + a.Add(this.Configuration.Token); + return await this.ApiClient.CreateGroupDmAsync(a, nicks); + } - /// - /// Deletes a stage instance in a stage channel. - /// - /// The ID of the channel to delete the stage instance of. - /// The reason the stage instance was deleted. - public Task DeleteStageInstanceAsync(ulong channelId, string reason = null) - => this.ApiClient.DeleteStageInstanceAsync(channelId, reason); - - /// - /// Pins a message. - /// - /// The ID of the channel the message is in. - /// The ID of the message. - public Task PinMessageAsync(ulong channelId, ulong messageId) - => this.ApiClient.PinMessageAsync(channelId, messageId); - - #endregion - - #region Member - /// - /// Gets current user object - /// - /// - public Task GetCurrentUserAsync() - => this.ApiClient.GetCurrentUserAsync(); - - /// - /// Gets user object - /// - /// User ID - /// - public Task GetUserAsync(ulong user) - => this.ApiClient.GetUserAsync(user); - - /// - /// Gets guild member - /// - /// Guild ID - /// Member ID - /// - public Task GetGuildMemberAsync(ulong guild_id, ulong member_id) - => this.ApiClient.GetGuildMemberAsync(guild_id, member_id); - - /// - /// Removes guild member - /// - /// Guild ID - /// User ID - /// Why this user was removed - /// - public Task RemoveGuildMemberAsync(ulong guild_id, ulong user_id, string reason) - => this.ApiClient.RemoveGuildMemberAsync(guild_id, user_id, reason); - - /// - /// Modifies current user - /// - /// New username - /// New avatar (base64) - /// - public async Task ModifyCurrentUserAsync(string username, string base64_avatar) - => new DiscordUser(await this.ApiClient.ModifyCurrentUserAsync(username, base64_avatar)) { Discord = this }; - - /// - /// Modifies current user - /// - /// username - /// avatar - /// - public async Task ModifyCurrentUserAsync(string username = null, Stream avatar = null) - { - string av64 = null; - if (avatar != null) - using (var imgtool = new ImageTool(avatar)) - av64 = imgtool.GetBase64(); + /// + /// Creates a DM + /// + /// Recipient user ID + /// + public async Task CreateDmAsync(ulong recipient_id) + => await this.ApiClient.CreateDmAsync(recipient_id); + + /// + /// Follows a news channel + /// + /// ID of the channel to follow + /// ID of the channel to crosspost messages to + /// Thrown when the current user doesn't have on the target channel + public async Task FollowChannelAsync(ulong channel_id, ulong webhook_channel_id) + => await this.ApiClient.FollowChannelAsync(channel_id, webhook_channel_id); + + /// + /// Publishes a message in a news channel to following channels + /// + /// ID of the news channel the message to crosspost belongs to + /// ID of the message to crosspost + /// + /// Thrown when the current user doesn't have and/or + /// + public async Task CrosspostMessageAsync(ulong channel_id, ulong message_id) + => await this.ApiClient.CrosspostMessageAsync(channel_id, message_id); + + /// + /// Creates a stage instance in a stage channel. + /// + /// The ID of the stage channel to create it in. + /// The topic of the stage instance. + /// The privacy level of the stage instance. + /// The reason the stage instance was created. + /// The created stage instance. + public async Task CreateStageInstanceAsync(ulong channelId, string topic, PrivacyLevel? privacyLevel = null, string reason = null) + => await this.ApiClient.CreateStageInstanceAsync(channelId, topic, privacyLevel, reason); + + /// + /// Gets a stage instance in a stage channel. + /// + /// The ID of the channel. + /// The stage instance in the channel. + public async Task GetStageInstanceAsync(ulong channelId) + => await this.ApiClient.GetStageInstanceAsync(channelId); + + /// + /// Modifies a stage instance in a stage channel. + /// + /// The ID of the channel to modify the stage instance of. + /// Action to perform. + /// The modified stage instance. + public async Task ModifyStageInstanceAsync(ulong channelId, Action action) + { + var mdl = new StageInstanceEditModel(); + action(mdl); + return await this.ApiClient.ModifyStageInstanceAsync(channelId, mdl.Topic, mdl.PrivacyLevel, mdl.AuditLogReason); + } - return new DiscordUser(await this.ApiClient.ModifyCurrentUserAsync(username, av64)) { Discord = this }; - } + /// + /// Deletes a stage instance in a stage channel. + /// + /// The ID of the channel to delete the stage instance of. + /// The reason the stage instance was deleted. + public async Task DeleteStageInstanceAsync(ulong channelId, string reason = null) + => await this.ApiClient.DeleteStageInstanceAsync(channelId, reason); + + /// + /// Pins a message. + /// + /// The ID of the channel the message is in. + /// The ID of the message. + public async Task PinMessageAsync(ulong channelId, ulong messageId) + => await this.ApiClient.PinMessageAsync(channelId, messageId); + + #endregion + + #region Member + /// + /// Gets current user object + /// + /// + public async Task GetCurrentUserAsync() + => await this.ApiClient.GetCurrentUserAsync(); + + /// + /// Gets user object + /// + /// User ID + /// + public async Task GetUserAsync(ulong user) + => await this.ApiClient.GetUserAsync(user); + + /// + /// Gets guild member + /// + /// Guild ID + /// Member ID + /// + public async Task GetGuildMemberAsync(ulong guild_id, ulong member_id) + => await this.ApiClient.GetGuildMemberAsync(guild_id, member_id); + + /// + /// Removes guild member + /// + /// Guild ID + /// User ID + /// Why this user was removed + /// + public async Task RemoveGuildMemberAsync(ulong guild_id, ulong user_id, string reason) + => await this.ApiClient.RemoveGuildMemberAsync(guild_id, user_id, reason); + + /// + /// Modifies current user + /// + /// New username + /// New avatar (base64) + /// + public async Task ModifyCurrentUserAsync(string username, string base64_avatar) + => new DiscordUser(await this.ApiClient.ModifyCurrentUserAsync(username, base64_avatar)) { Discord = this }; + + /// + /// Modifies current user + /// + /// username + /// avatar + /// + public async Task ModifyCurrentUserAsync(string username = null, Stream avatar = null) + { + string av64 = null; + if (avatar != null) + using (var imgtool = new ImageTool(avatar)) + av64 = imgtool.GetBase64(); - /// - /// Gets current user's guilds - /// - /// Limit of guilds to get - /// Gets guild before ID - /// Gets guilds after ID - /// - public Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) - => this.ApiClient.GetCurrentUserGuildsAsync(limit, before, after); - - /// - /// Modifies guild member. - /// - /// Guild ID - /// User ID - /// New nickname - /// New roles - /// Whether this user should be muted - /// Whether this user should be deafened - /// Voice channel to move this user to - /// How long this member should be timed out for. Requires MODERATE_MEMBERS permission. - /// Reason this user was modified - /// - public Task ModifyGuildMemberAsync(ulong guild_id, ulong user_id, Optional nick, - Optional> role_ids, Optional mute, Optional deaf, - Optional voice_channel_id, Optional communication_disabled_until, string reason) - => this.ApiClient.ModifyGuildMemberAsync(guild_id, user_id, nick, role_ids, mute, deaf, voice_channel_id, communication_disabled_until, reason); - - /// - /// Modifies a member - /// - /// Member ID - /// Guild ID - /// Modifications - /// - public async Task ModifyAsync(ulong member_id, ulong guild_id, Action action) - { - var mdl = new MemberEditModel(); - action(mdl); + return new DiscordUser(await this.ApiClient.ModifyCurrentUserAsync(username, av64)) { Discord = this }; + } - if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value != null && mdl.VoiceChannel.Value.Type != ChannelType.Voice && mdl.VoiceChannel.Value.Type != ChannelType.Stage) - throw new ArgumentException("Given channel is not a voice or stage channel.", nameof(mdl.VoiceChannel)); + /// + /// Gets current user's guilds + /// + /// Limit of guilds to get + /// Gets guild before ID + /// Gets guilds after ID + /// + public async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) + => await this.ApiClient.GetCurrentUserGuildsAsync(limit, before, after); + + /// + /// Modifies guild member. + /// + /// Guild ID + /// User ID + /// New nickname + /// New roles + /// Whether this user should be muted + /// Whether this user should be deafened + /// Voice channel to move this user to + /// How long this member should be timed out for. Requires MODERATE_MEMBERS permission. + /// Reason this user was modified + /// + public async Task ModifyGuildMemberAsync(ulong guild_id, ulong user_id, Optional nick, + Optional> role_ids, Optional mute, Optional deaf, + Optional voice_channel_id, Optional communication_disabled_until, string reason) + => await this.ApiClient.ModifyGuildMemberAsync(guild_id, user_id, nick, role_ids, mute, deaf, voice_channel_id, communication_disabled_until, reason); + + /// + /// Modifies a member + /// + /// Member ID + /// Guild ID + /// Modifications + /// + public async Task ModifyAsync(ulong member_id, ulong guild_id, Action action) + { + var mdl = new MemberEditModel(); + action(mdl); - if (mdl.Nickname.HasValue && this.CurrentUser.Id == member_id) - { - await this.ApiClient.ModifyCurrentMemberAsync(guild_id, mdl.Nickname.Value, - mdl.AuditLogReason); - await this.ApiClient.ModifyGuildMemberAsync(guild_id, member_id, Optional.FromNoValue(), - mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, - mdl.VoiceChannel.IfPresent(e => e?.Id), default, mdl.AuditLogReason); - } - else - { - await this.ApiClient.ModifyGuildMemberAsync(guild_id, member_id, mdl.Nickname, - mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, - mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.CommunicationDisabledUntil, mdl.AuditLogReason); - } - } - - - /// - /// Changes the current user in a guild. - /// - /// Guild ID - /// Nickname to set - /// Audit log reason - /// - public Task ModifyCurrentMemberAsync(ulong guild_id, string nickname, string reason) - => this.ApiClient.ModifyCurrentMemberAsync(guild_id, nickname, reason); - - #endregion - - #region Roles - /// - /// Gets roles - /// - /// Guild ID - /// - public Task> GetGuildRolesAsync(ulong guild_id) - => this.ApiClient.GetGuildRolesAsync(guild_id); - - /// - /// Gets a guild. - /// - /// The guild ID to search for. - /// Whether to include approximate presence and member counts in the returned guild. - /// - public Task GetGuildAsync(ulong guild_id, bool? with_counts = null) - => this.ApiClient.GetGuildAsync(guild_id, with_counts); - - /// - /// Modifies a role - /// - /// Guild ID - /// Role ID - /// New role name - /// New role permissions - /// New role color - /// Whether this role should be hoisted - /// Whether this role should be mentionable - /// Why this role was modified - /// The icon to add to this role - /// The emoji to add to this role. Must be unicode. - /// - public Task ModifyGuildRoleAsync(ulong guild_id, ulong role_id, string name, Permissions? permissions, DiscordColor? color, bool? hoist, bool? mentionable, string reason, Stream icon, DiscordEmoji emoji) - => this.ApiClient.ModifyGuildRoleAsync(guild_id, role_id, name, permissions, color.HasValue ? (int?)color.Value.Value : null, hoist, mentionable, reason, icon, emoji?.ToString()); - - /// - /// Modifies a role - /// - /// Role ID - /// Guild ID - /// Modifications - /// - public Task ModifyGuildRoleAsync(ulong role_id, ulong guild_id, Action action) - { - var mdl = new RoleEditModel(); - action(mdl); + if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value != null && mdl.VoiceChannel.Value.Type != ChannelType.Voice && mdl.VoiceChannel.Value.Type != ChannelType.Stage) + throw new ArgumentException("Given channel is not a voice or stage channel.", nameof(mdl.VoiceChannel)); - return this.ModifyGuildRoleAsync(guild_id, role_id, mdl.Name, mdl.Permissions, mdl.Color, mdl.Hoist, mdl.Mentionable, mdl.AuditLogReason, mdl.Icon, mdl.Emoji); + if (mdl.Nickname.HasValue && this.CurrentUser.Id == member_id) + { + await this.ApiClient.ModifyCurrentMemberAsync(guild_id, mdl.Nickname.Value, + mdl.AuditLogReason); + await this.ApiClient.ModifyGuildMemberAsync(guild_id, member_id, Optional.FromNoValue(), + mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, + mdl.VoiceChannel.IfPresent(e => e?.Id), default, mdl.AuditLogReason); } - - /// - /// Deletes a role - /// - /// Guild ID - /// Role ID - /// Reason why this role was deleted - /// - public Task DeleteGuildRoleAsync(ulong guild_id, ulong role_id, string reason) - => this.ApiClient.DeleteRoleAsync(guild_id, role_id, reason); - - /// - /// Creates a new role - /// - /// Guild ID - /// Role name - /// Role permissions - /// Role color - /// Whether this role should be hoisted - /// Whether this role should be mentionable - /// Reason why this role was created - /// The icon to add to this role - /// The emoji to add to this role. Must be unicode. - /// - public Task CreateGuildRoleAsync(ulong guild_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason, Stream icon = null, DiscordEmoji emoji = null) - => this.ApiClient.CreateGuildRoleAsync(guild_id, name, permissions, color, hoist, mentionable, reason, icon, emoji?.ToString()); - #endregion - - #region Prune - /// - /// Get a guild's prune count. - /// - /// Guild ID - /// Days to check for - /// The roles to be included in the prune. - /// - public Task GetGuildPruneCountAsync(ulong guild_id, int days, IEnumerable include_roles) - => this.ApiClient.GetGuildPruneCountAsync(guild_id, days, include_roles); - - /// - /// Begins a guild prune. - /// - /// Guild ID - /// Days to prune for - /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. - /// The roles to be included in the prune. - /// Reason why this guild was pruned - /// - public Task BeginGuildPruneAsync(ulong guild_id, int days, bool compute_prune_count, IEnumerable include_roles, string reason) - => this.ApiClient.BeginGuildPruneAsync(guild_id, days, compute_prune_count, include_roles, reason); - #endregion - - #region GuildVarious - /// - /// Gets guild integrations - /// - /// Guild ID - /// - public Task> GetGuildIntegrationsAsync(ulong guild_id) - => this.ApiClient.GetGuildIntegrationsAsync(guild_id); - - /// - /// Creates guild integration - /// - /// Guild ID - /// Integration type - /// Integration id - /// - public Task CreateGuildIntegrationAsync(ulong guild_id, string type, ulong id) - => this.ApiClient.CreateGuildIntegrationAsync(guild_id, type, id); - - /// - /// Modifies a guild integration - /// - /// Guild ID - /// Integration ID - /// Expiration behaviour - /// Expiration grace period - /// Whether to enable emojis for this integration - /// - public Task ModifyGuildIntegrationAsync(ulong guild_id, ulong integration_id, int expire_behaviour, int expire_grace_period, bool enable_emoticons) - => this.ApiClient.ModifyGuildIntegrationAsync(guild_id, integration_id, expire_behaviour, expire_grace_period, enable_emoticons); - - /// - /// Removes a guild integration - /// - /// Guild ID - /// Integration to remove - /// Reason why this integration was removed - /// - public Task DeleteGuildIntegrationAsync(ulong guild_id, DiscordIntegration integration, string reason = null) - => this.ApiClient.DeleteGuildIntegrationAsync(guild_id, integration, reason); - - /// - /// Syncs guild integration - /// - /// Guild ID - /// Integration ID - /// - public Task SyncGuildIntegrationAsync(ulong guild_id, ulong integration_id) - => this.ApiClient.SyncGuildIntegrationAsync(guild_id, integration_id); - - /// - /// Get a guild's voice region - /// - /// Guild ID - /// - public Task> GetGuildVoiceRegionsAsync(ulong guild_id) - => this.ApiClient.GetGuildVoiceRegionsAsync(guild_id); - - /// - /// Get a guild's invites - /// - /// Guild ID - /// - public Task> GetGuildInvitesAsync(ulong guild_id) - => this.ApiClient.GetGuildInvitesAsync(guild_id); - - /// - /// Gets a guild's templates. - /// - /// Guild ID - /// All of the guild's templates. - public Task> GetGuildTemplatesAsync(ulong guild_id) - => this.ApiClient.GetGuildTemplatesAsync(guild_id); - - /// - /// Creates a guild template. - /// - /// Guild ID - /// Name of the template. - /// Description of the template. - /// The template created. - public Task CreateGuildTemplateAsync(ulong guild_id, string name, string description = null) - => this.ApiClient.CreateGuildTemplateAsync(guild_id, name, description); - - /// - /// Syncs the template to the current guild's state. - /// - /// Guild ID - /// The code of the template to sync. - /// The template synced. - public Task SyncGuildTemplateAsync(ulong guild_id, string code) - => this.ApiClient.SyncGuildTemplateAsync(guild_id, code); - - /// - /// Modifies the template's metadata. - /// - /// Guild ID - /// The template's code. - /// Name of the template. - /// Description of the template. - /// The template modified. - public Task ModifyGuildTemplateAsync(ulong guild_id, string code, string name = null, string description = null) - => this.ApiClient.ModifyGuildTemplateAsync(guild_id, code, name, description); - - /// - /// Deletes the template. - /// - /// Guild ID - /// The code of the template to delete. - /// The deleted template. - public Task DeleteGuildTemplateAsync(ulong guild_id, string code) - => this.ApiClient.DeleteGuildTemplateAsync(guild_id, code); - - /// - /// Gets a guild's welcome screen. - /// - /// The guild's welcome screen object. - public Task GetGuildWelcomeScreenAsync(ulong guildId) => - this.ApiClient.GetGuildWelcomeScreenAsync(guildId); - - /// - /// Modifies a guild's welcome screen. - /// - /// The guild ID to modify. - /// Action to perform. - /// The audit log reason for this action. - /// The modified welcome screen. - public async Task ModifyGuildWelcomeScreenAsync(ulong guildId, Action action, string reason = null) + else { - var mdl = new WelcomeScreenEditModel(); - action(mdl); - return await this.ApiClient.ModifyGuildWelcomeScreenAsync(guildId, mdl.Enabled, mdl.WelcomeChannels, mdl.Description, reason); + await this.ApiClient.ModifyGuildMemberAsync(guild_id, member_id, mdl.Nickname, + mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, + mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.CommunicationDisabledUntil, mdl.AuditLogReason); } + } + + + /// + /// Changes the current user in a guild. + /// + /// Guild ID + /// Nickname to set + /// Audit log reason + /// + public async Task ModifyCurrentMemberAsync(ulong guild_id, string nickname, string reason) + => await this.ApiClient.ModifyCurrentMemberAsync(guild_id, nickname, reason); + + #endregion + + #region Roles + /// + /// Gets roles + /// + /// Guild ID + /// + public async Task> GetGuildRolesAsync(ulong guild_id) + => await this.ApiClient.GetGuildRolesAsync(guild_id); + + /// + /// Gets a guild. + /// + /// The guild ID to search for. + /// Whether to include approximate presence and member counts in the returned guild. + /// + public async Task GetGuildAsync(ulong guild_id, bool? with_counts = null) + => await this.ApiClient.GetGuildAsync(guild_id, with_counts); + + /// + /// Modifies a role + /// + /// Guild ID + /// Role ID + /// New role name + /// New role permissions + /// New role color + /// Whether this role should be hoisted + /// Whether this role should be mentionable + /// Why this role was modified + /// The icon to add to this role + /// The emoji to add to this role. Must be unicode. + /// + public async Task ModifyGuildRoleAsync(ulong guild_id, ulong role_id, string name, Permissions? permissions, DiscordColor? color, bool? hoist, bool? mentionable, string reason, Stream icon, DiscordEmoji emoji) + => await this.ApiClient.ModifyGuildRoleAsync(guild_id, role_id, name, permissions, color.HasValue ? (int?)color.Value.Value : null, hoist, mentionable, icon, emoji?.ToString(), reason); + + /// + /// Modifies a role + /// + /// Role ID + /// Guild ID + /// Modifications + /// + public async Task ModifyGuildRoleAsync(ulong role_id, ulong guild_id, Action action) + { + var mdl = new RoleEditModel(); + action(mdl); - /// - /// Gets a guild preview. - /// - /// The ID of the guild. - public Task GetGuildPreviewAsync(ulong guildId) - => this.ApiClient.GetGuildPreviewAsync(guildId); - - #endregion - - #region Invites - /// - /// Gets an invite. - /// - /// The invite code. - /// Whether to include presence and total member counts in the returned invite. - /// Whether to include the expiration date in the returned invite. - /// - public Task GetInviteAsync(string invite_code, bool? withCounts = null, bool? withExpiration = null) - => this.ApiClient.GetInviteAsync(invite_code, withCounts, withExpiration); - - /// - /// Removes an invite - /// - /// Invite code - /// Reason why this invite was removed - /// - public Task DeleteInvite(string invite_code, string reason) - => this.ApiClient.DeleteInviteAsync(invite_code, reason); - #endregion - - #region Connections - /// - /// Gets current user's connections - /// - /// - public Task> GetUsersConnectionsAsync() - => this.ApiClient.GetUsersConnectionsAsync(); - #endregion - - #region Webhooks - /// - /// Creates a new webhook - /// - /// Channel ID - /// Webhook name - /// Webhook avatar (base64) - /// Reason why this webhook was created - /// - public Task CreateWebhookAsync(ulong channel_id, string name, string base64_avatar, string reason) - => this.ApiClient.CreateWebhookAsync(channel_id, name, base64_avatar, reason); - - /// - /// Creates a new webhook - /// - /// Channel ID - /// Webhook name - /// Webhook avatar - /// Reason why this webhook was created - /// - public Task CreateWebhookAsync(ulong channel_id, string name, Stream avatar = null, string reason = null) - { - string av64 = null; - if (avatar != null) - using (var imgtool = new ImageTool(avatar)) - av64 = imgtool.GetBase64(); + await this.ModifyGuildRoleAsync(guild_id, role_id, mdl.Name, mdl.Permissions, mdl.Color, mdl.Hoist, mdl.Mentionable, mdl.AuditLogReason, mdl.Icon, mdl.Emoji); + } - return this.ApiClient.CreateWebhookAsync(channel_id, name, av64, reason); - } + /// + /// Deletes a role + /// + /// Guild ID + /// Role ID + /// Reason why this role was deleted + /// + public async Task DeleteGuildRoleAsync(ulong guild_id, ulong role_id, string reason) + => await this.ApiClient.DeleteRoleAsync(guild_id, role_id, reason); + + /// + /// Creates a new role + /// + /// Guild ID + /// Role name + /// Role permissions + /// Role color + /// Whether this role should be hoisted + /// Whether this role should be mentionable + /// Reason why this role was created + /// The icon to add to this role + /// The emoji to add to this role. Must be unicode. + /// + public async Task CreateGuildRoleAsync(ulong guild_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason, Stream icon = null, DiscordEmoji emoji = null) + => await this.ApiClient.CreateGuildRoleAsync(guild_id, name, permissions, color, hoist, mentionable, icon, emoji?.ToString(), reason); + #endregion + + #region Prune + /// + /// Get a guild's prune count. + /// + /// Guild ID + /// Days to check for + /// The roles to be included in the prune. + /// + public async Task GetGuildPruneCountAsync(ulong guild_id, int days, IEnumerable include_roles) + => await this.ApiClient.GetGuildPruneCountAsync(guild_id, days, include_roles); + + /// + /// Begins a guild prune. + /// + /// Guild ID + /// Days to prune for + /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. + /// The roles to be included in the prune. + /// Reason why this guild was pruned + /// + public async Task BeginGuildPruneAsync(ulong guild_id, int days, bool compute_prune_count, IEnumerable include_roles, string reason) + => await this.ApiClient.BeginGuildPruneAsync(guild_id, days, compute_prune_count, include_roles, reason); + #endregion + + #region GuildVarious + /// + /// Gets guild integrations + /// + /// Guild ID + /// + public async Task> GetGuildIntegrationsAsync(ulong guild_id) + => await this.ApiClient.GetGuildIntegrationsAsync(guild_id); + + /// + /// Creates guild integration + /// + /// Guild ID + /// Integration type + /// Integration id + /// + public async Task CreateGuildIntegrationAsync(ulong guild_id, string type, ulong id) + => await this.ApiClient.CreateGuildIntegrationAsync(guild_id, type, id); + + /// + /// Modifies a guild integration + /// + /// Guild ID + /// Integration ID + /// Expiration behaviour + /// Expiration grace period + /// Whether to enable emojis for this integration + /// + public async Task ModifyGuildIntegrationAsync(ulong guild_id, ulong integration_id, int expire_behaviour, int expire_grace_period, bool enable_emoticons) + => await this.ApiClient.ModifyGuildIntegrationAsync(guild_id, integration_id, expire_behaviour, expire_grace_period, enable_emoticons); + + /// + /// Removes a guild integration + /// + /// Guild ID + /// Integration to remove + /// Reason why this integration was removed + /// + public async Task DeleteGuildIntegrationAsync(ulong guild_id, DiscordIntegration integration, string reason = null) + => await this.ApiClient.DeleteGuildIntegrationAsync(guild_id, integration.Id, reason); + + /// + /// Syncs guild integration + /// + /// Guild ID + /// Integration ID + /// + public async Task SyncGuildIntegrationAsync(ulong guild_id, ulong integration_id) + => await this.ApiClient.SyncGuildIntegrationAsync(guild_id, integration_id); + + /// + /// Get a guild's voice region + /// + /// Guild ID + /// + public async Task> GetGuildVoiceRegionsAsync(ulong guild_id) + => await this.ApiClient.GetGuildVoiceRegionsAsync(guild_id); + + /// + /// Get a guild's invites + /// + /// Guild ID + /// + public async Task> GetGuildInvitesAsync(ulong guild_id) + => await this.ApiClient.GetGuildInvitesAsync(guild_id); + + /// + /// Gets a guild's templates. + /// + /// Guild ID + /// All of the guild's templates. + public async Task> GetGuildTemplatesAsync(ulong guild_id) + => await this.ApiClient.GetGuildTemplatesAsync(guild_id); + + /// + /// Creates a guild template. + /// + /// Guild ID + /// Name of the template. + /// Description of the template. + /// The template created. + public async Task CreateGuildTemplateAsync(ulong guild_id, string name, string description = null) + => await this.ApiClient.CreateGuildTemplateAsync(guild_id, name, description); + + /// + /// Syncs the template to the current guild's state. + /// + /// Guild ID + /// The code of the template to sync. + /// The template synced. + public async Task SyncGuildTemplateAsync(ulong guild_id, string code) + => await this.ApiClient.SyncGuildTemplateAsync(guild_id, code); + + /// + /// Modifies the template's metadata. + /// + /// Guild ID + /// The template's code. + /// Name of the template. + /// Description of the template. + /// The template modified. + public async Task ModifyGuildTemplateAsync(ulong guild_id, string code, string name = null, string description = null) + => await this.ApiClient.ModifyGuildTemplateAsync(guild_id, code, name, description); + + /// + /// Deletes the template. + /// + /// Guild ID + /// The code of the template to delete. + /// The deleted template. + public async Task DeleteGuildTemplateAsync(ulong guild_id, string code) + => await this.ApiClient.DeleteGuildTemplateAsync(guild_id, code); + + /// + /// Gets a guild's welcome screen. + /// + /// The guild's welcome screen object. + public async Task GetGuildWelcomeScreenAsync(ulong guildId) => + await this.ApiClient.GetGuildWelcomeScreenAsync(guildId); + + /// + /// Modifies a guild's welcome screen. + /// + /// The guild ID to modify. + /// Action to perform. + /// The audit log reason for this action. + /// The modified welcome screen. + public async Task ModifyGuildWelcomeScreenAsync(ulong guildId, Action action, string reason = null) + { + var mdl = new WelcomeScreenEditModel(); + action(mdl); + return await this.ApiClient.ModifyGuildWelcomeScreenAsync(guildId, mdl.Enabled, mdl.WelcomeChannels, mdl.Description, reason); + } - /// - /// Gets all webhooks from a channel - /// - /// Channel ID - /// - public Task> GetChannelWebhooksAsync(ulong channel_id) - => this.ApiClient.GetChannelWebhooksAsync(channel_id); - - /// - /// Gets all webhooks from a guild - /// - /// Guild ID - /// - public Task> GetGuildWebhooksAsync(ulong guild_id) - => this.ApiClient.GetGuildWebhooksAsync(guild_id); - - /// - /// Gets a webhook - /// - /// Webhook ID - /// - public Task GetWebhookAsync(ulong webhook_id) - => this.ApiClient.GetWebhookAsync(webhook_id); - - /// - /// Gets a webhook with its token (when user is not in said guild) - /// - /// Webhook ID - /// Webhook token - /// - public Task GetWebhookWithTokenAsync(ulong webhook_id, string webhook_token) - => this.ApiClient.GetWebhookWithTokenAsync(webhook_id, webhook_token); - - /// - /// Modifies a webhook - /// - /// Webhook ID - /// The new channel ID the webhook should be moved to. - /// New webhook name - /// New webhook avatar (base64) - /// Reason why this webhook was modified - /// - public Task ModifyWebhookAsync(ulong webhook_id, ulong channelId, string name, string base64_avatar, string reason) - => this.ApiClient.ModifyWebhookAsync(webhook_id, channelId, name, base64_avatar, reason); - - /// - /// Modifies a webhook - /// - /// Webhook ID - /// The new channel ID the webhook should be moved to. - /// New webhook name - /// New webhook avatar - /// Reason why this webhook was modified - /// - public Task ModifyWebhookAsync(ulong webhook_id, ulong channelId, string name, Stream avatar, string reason) - { - string av64 = null; - if (avatar != null) - using (var imgtool = new ImageTool(avatar)) - av64 = imgtool.GetBase64(); + /// + /// Gets a guild preview. + /// + /// The ID of the guild. + public async Task GetGuildPreviewAsync(ulong guildId) + => await this.ApiClient.GetGuildPreviewAsync(guildId); + + #endregion + + #region Invites + /// + /// Gets an invite. + /// + /// The invite code. + /// Whether to include presence and total member counts in the returned invite. + /// Whether to include the expiration date in the returned invite. + /// + public async Task GetInviteAsync(string invite_code, bool? withCounts = null, bool? withExpiration = null) + => await this.ApiClient.GetInviteAsync(invite_code, withCounts, withExpiration); + + /// + /// Removes an invite + /// + /// Invite code + /// Reason why this invite was removed + /// + public async Task DeleteInviteAsync(string invite_code, string reason) + => await this.ApiClient.DeleteInviteAsync(invite_code, reason); + #endregion + + #region Connections + /// + /// Gets current user's connections + /// + /// + public async Task> GetUsersConnectionsAsync() + => await this.ApiClient.GetUsersConnectionsAsync(); + #endregion + + #region Webhooks + /// + /// Creates a new webhook + /// + /// Channel ID + /// Webhook name + /// Webhook avatar (base64) + /// Reason why this webhook was created + /// + public async Task CreateWebhookAsync(ulong channel_id, string name, string base64_avatar, string reason) + => await this.ApiClient.CreateWebhookAsync(channel_id, name, base64_avatar, reason); + + /// + /// Creates a new webhook + /// + /// Channel ID + /// Webhook name + /// Webhook avatar + /// Reason why this webhook was created + /// + public async Task CreateWebhookAsync(ulong channel_id, string name, Stream avatar = null, string reason = null) + { + string av64 = null; + if (avatar != null) + using (var imgtool = new ImageTool(avatar)) + av64 = imgtool.GetBase64(); - return this.ApiClient.ModifyWebhookAsync(webhook_id, channelId, name, av64, reason); - } + return await this.ApiClient.CreateWebhookAsync(channel_id, name, av64, reason); + } - /// - /// Modifies a webhook (when user is not in said guild) - /// - /// Webhook ID - /// New webhook name - /// New webhook avatar (base64) - /// Webhook token - /// Reason why this webhook was modified - /// - public Task ModifyWebhookAsync(ulong webhook_id, string name, string base64_avatar, string webhook_token, string reason) - => this.ApiClient.ModifyWebhookAsync(webhook_id, name, base64_avatar, webhook_token, reason); - - /// - /// Modifies a webhook (when user is not in said guild) - /// - /// Webhook ID - /// New webhook name - /// New webhook avatar - /// Webhook token - /// Reason why this webhook was modified - /// - public Task ModifyWebhookAsync(ulong webhook_id, string name, Stream avatar, string webhook_token, string reason) - { - string av64 = null; - if (avatar != null) - using (var imgtool = new ImageTool(avatar)) - av64 = imgtool.GetBase64(); + /// + /// Gets all webhooks from a channel + /// + /// Channel ID + /// + public async Task> GetChannelWebhooksAsync(ulong channel_id) + => await this.ApiClient.GetChannelWebhooksAsync(channel_id); + + /// + /// Gets all webhooks from a guild + /// + /// Guild ID + /// + public async Task> GetGuildWebhooksAsync(ulong guild_id) + => await this.ApiClient.GetGuildWebhooksAsync(guild_id); + + /// + /// Gets a webhook + /// + /// Webhook ID + /// + public async Task GetWebhookAsync(ulong webhook_id) + => await this.ApiClient.GetWebhookAsync(webhook_id); + + /// + /// Gets a webhook with its token (when user is not in said guild) + /// + /// Webhook ID + /// Webhook token + /// + public async Task GetWebhookWithTokenAsync(ulong webhook_id, string webhook_token) + => await this.ApiClient.GetWebhookWithTokenAsync(webhook_id, webhook_token); + + /// + /// Modifies a webhook + /// + /// Webhook ID + /// The new channel ID the webhook should be moved to. + /// New webhook name + /// New webhook avatar (base64) + /// Reason why this webhook was modified + /// + public async Task ModifyWebhookAsync(ulong webhook_id, ulong channelId, string name, string base64_avatar, string reason) + => await this.ApiClient.ModifyWebhookAsync(webhook_id, channelId, name, base64_avatar, reason); + + /// + /// Modifies a webhook + /// + /// Webhook ID + /// The new channel ID the webhook should be moved to. + /// New webhook name + /// New webhook avatar + /// Reason why this webhook was modified + /// + public async Task ModifyWebhookAsync(ulong webhook_id, ulong channelId, string name, Stream avatar, string reason) + { + string av64 = null; + if (avatar != null) + using (var imgtool = new ImageTool(avatar)) + av64 = imgtool.GetBase64(); - return this.ApiClient.ModifyWebhookAsync(webhook_id, name, av64, webhook_token, reason); - } + return await this.ApiClient.ModifyWebhookAsync(webhook_id, channelId, name, av64, reason); + } - /// - /// Deletes a webhook - /// - /// Webhook ID - /// Reason this webhook was deleted - /// - public Task DeleteWebhookAsync(ulong webhook_id, string reason) - => this.ApiClient.DeleteWebhookAsync(webhook_id, reason); - - /// - /// Deletes a webhook (when user is not in said guild) - /// - /// Webhook ID - /// Reason this webhook was removed - /// Webhook token - /// - public Task DeleteWebhookAsync(ulong webhook_id, string reason, string webhook_token) - => this.ApiClient.DeleteWebhookAsync(webhook_id, webhook_token, reason); - - /// - /// Sends a message to a webhook - /// - /// Webhook ID - /// Webhook token - /// Webhook builder filled with data to send. - /// - public Task ExecuteWebhookAsync(ulong webhook_id, string webhook_token, DiscordWebhookBuilder builder) - => this.ApiClient.ExecuteWebhookAsync(webhook_id, webhook_token, builder); - - /// - /// Edits a previously-sent webhook message. - /// - /// Webhook ID - /// Webhook token - /// The ID of the message to edit. - /// The builder of the message to edit. - /// Attached files to keep. - /// The modified - public async Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(true); + /// + /// Modifies a webhook (when user is not in said guild) + /// + /// Webhook ID + /// New webhook name + /// New webhook avatar (base64) + /// Webhook token + /// Reason why this webhook was modified + /// + public async Task ModifyWebhookAsync(ulong webhook_id, string name, string base64_avatar, string webhook_token, string reason) + => await this.ApiClient.ModifyWebhookAsync(webhook_id, name, base64_avatar, webhook_token, reason); + + /// + /// Modifies a webhook (when user is not in said guild) + /// + /// Webhook ID + /// New webhook name + /// New webhook avatar + /// Webhook token + /// Reason why this webhook was modified + /// + public async Task ModifyWebhookAsync(ulong webhook_id, string name, Stream avatar, string webhook_token, string reason) + { + string av64 = null; + if (avatar != null) + using (var imgtool = new ImageTool(avatar)) + av64 = imgtool.GetBase64(); - return await this.ApiClient.EditWebhookMessageAsync(webhook_id, webhook_token, messageId, builder, attachments); - } + return await this.ApiClient.ModifyWebhookAsync(webhook_id, name, av64, webhook_token, reason); + } - /// - /// Deletes a message that was created by a webhook. - /// - /// Webhook ID - /// Webhook token - /// The ID of the message to delete - /// - public Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong messageId) - => this.ApiClient.DeleteWebhookMessageAsync(webhook_id, webhook_token, messageId); - #endregion - - #region Reactions - /// - /// Creates a new reaction - /// - /// Channel ID - /// Message ID - /// Emoji to react - /// - public Task CreateReactionAsync(ulong channel_id, ulong message_id, string emoji) - => this.ApiClient.CreateReactionAsync(channel_id, message_id, emoji); - - /// - /// Deletes own reaction - /// - /// Channel ID - /// Message ID - /// Emoji to remove from reaction - /// - public Task DeleteOwnReactionAsync(ulong channel_id, ulong message_id, string emoji) - => this.ApiClient.DeleteOwnReactionAsync(channel_id, message_id, emoji); - - /// - /// Deletes someone elses reaction - /// - /// Channel ID - /// Message ID - /// User ID - /// Emoji to remove - /// Reason why this reaction was removed - /// - public Task DeleteUserReactionAsync(ulong channel_id, ulong message_id, ulong user_id, string emoji, string reason) - => this.ApiClient.DeleteUserReactionAsync(channel_id, message_id, user_id, emoji, reason); - - /// - /// Gets all users that reacted with a specific emoji to a message - /// - /// Channel ID - /// Message ID - /// Emoji to check for - /// Whether to search for reactions after this message id. - /// The maximum amount of reactions to fetch. - /// - public Task> GetReactionsAsync(ulong channel_id, ulong message_id, string emoji, ulong? after_id = null, int limit = 25) - => this.ApiClient.GetReactionsAsync(channel_id, message_id, emoji, after_id, limit); - - /// - /// Gets all users that reacted with a specific emoji to a message - /// - /// Channel ID - /// Message ID - /// Emoji to check for - /// Whether to search for reactions after this message id. - /// The maximum amount of reactions to fetch. - /// - public Task> GetReactionsAsync(ulong channel_id, ulong message_id, DiscordEmoji emoji, ulong? after_id = null, int limit = 25) - => this.ApiClient.GetReactionsAsync(channel_id, message_id, emoji.ToReactionString(), after_id, limit); - - /// - /// Deletes all reactions from a message - /// - /// Channel ID - /// Message ID - /// Reason why all reactions were removed - /// - public Task DeleteAllReactionsAsync(ulong channel_id, ulong message_id, string reason) - => this.ApiClient.DeleteAllReactionsAsync(channel_id, message_id, reason); - - /// - /// Deletes all reactions of a specific reaction for a message. - /// - /// The ID of the channel. - /// The ID of the message. - /// The emoji to clear. - /// - public Task DeleteReactionsEmojiAsync(ulong channelid, ulong messageId, string emoji) - => this.ApiClient.DeleteReactionsEmojiAsync(channelid, messageId, emoji); - - #endregion - - #region Application Commands - /// - /// Gets all the global application commands for this application. - /// - /// A list of global application commands. - public Task> GetGlobalApplicationCommandsAsync() => - this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id); - - /// - /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. - /// - /// The list of commands to overwrite with. - /// The list of global commands. - public Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => - this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); - - /// - /// Creates or overwrites a global application command. - /// - /// The command to create. - /// The created command. - public Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => - this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); - - /// - /// Gets a global application command by its ID. - /// - /// The ID of the command to get. - /// The command with the ID. - public Task GetGlobalApplicationCommandAsync(ulong commandId) => - this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); - - /// - /// Edits a global application command. - /// - /// The ID of the command to edit. - /// Action to perform. - /// The edited command. - public async Task EditGlobalApplicationCommandAsync(ulong commandId, Action action) - { - var mdl = new ApplicationCommandEditModel(); - action(mdl); - var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync()).Id; - return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); - } + /// + /// Deletes a webhook + /// + /// Webhook ID + /// Reason this webhook was deleted + /// + public async Task DeleteWebhookAsync(ulong webhook_id, string reason) + => await this.ApiClient.DeleteWebhookAsync(webhook_id, reason); + + /// + /// Deletes a webhook (when user is not in said guild) + /// + /// Webhook ID + /// Reason this webhook was removed + /// Webhook token + /// + public async Task DeleteWebhookAsync(ulong webhook_id, string reason, string webhook_token) + => await this.ApiClient.DeleteWebhookAsync(webhook_id, webhook_token, reason); + + /// + /// Sends a message to a webhook + /// + /// Webhook ID + /// Webhook token + /// Webhook builder filled with data to send. + /// + public async Task ExecuteWebhookAsync(ulong webhook_id, string webhook_token, DiscordWebhookBuilder builder) + => await this.ApiClient.ExecuteWebhookAsync(webhook_id, webhook_token, builder); + + /// + /// Edits a previously-sent webhook message. + /// + /// Webhook ID + /// Webhook token + /// The ID of the message to edit. + /// The builder of the message to edit. + /// Attached files to keep. + /// The modified + public async Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(true); - /// - /// Deletes a global application command. - /// - /// The ID of the command to delete. - public Task DeleteGlobalApplicationCommandAsync(ulong commandId) => - this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); - - /// - /// Gets all the application commands for a guild. - /// - /// The ID of the guild to get application commands for. - /// A list of application commands in the guild. - public Task> GetGuildApplicationCommandsAsync(ulong guildId) => - this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId); - - /// - /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. - /// - /// The ID of the guild. - /// The list of commands to overwrite with. - /// The list of guild commands. - public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => - this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); - - /// - /// Creates or overwrites a guild application command. - /// - /// The ID of the guild to create the application command in. - /// The command to create. - /// The created command. - public Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => - this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); - - /// - /// Gets a application command in a guild by its ID. - /// - /// The ID of the guild the application command is in. - /// The ID of the command to get. - /// The command with the ID. - public Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); - - /// - /// Edits a application command in a guild. - /// - /// The ID of the guild the application command is in. - /// The ID of the command to edit. - /// Action to perform. - /// The edited command. - public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, Action action) - { - var mdl = new ApplicationCommandEditModel(); - action(mdl); - var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync()).Id; - return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); - } + return await this.ApiClient.EditWebhookMessageAsync(webhook_id, webhook_token, messageId, builder, attachments); + } - /// - /// Deletes a application command in a guild. - /// - /// The ID of the guild to delete the application command in. - /// The ID of the command. - public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); - - /// - /// Creates a response to an interaction. - /// - /// The ID of the interaction. - /// The token of the interaction - /// The type of the response. - /// The data, if any, to send. - public Task CreateInteractionResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, DiscordInteractionResponseBuilder builder = null) => - this.ApiClient.CreateInteractionResponseAsync(interactionId, interactionToken, type, builder); - - /// - /// Gets the original interaction response. - /// - /// The original message that was sent. This does not work on ephemeral messages. - public Task GetOriginalInteractionResponseAsync(string interactionToken) => - this.ApiClient.GetOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken); - - /// - /// Edits the original interaction response. - /// - /// The token of the interaction. - /// The webhook builder. - /// Attached files to keep. - /// The edited. - public async Task EditOriginalInteractionResponseAsync(string interactionToken, DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(isInteractionResponse: true); + /// + /// Deletes a message that was created by a webhook. + /// + /// Webhook ID + /// Webhook token + /// The ID of the message to delete + /// + public async Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong messageId) + => await this.ApiClient.DeleteWebhookMessageAsync(webhook_id, webhook_token, messageId); + #endregion + + #region Reactions + /// + /// Creates a new reaction + /// + /// Channel ID + /// Message ID + /// Emoji to react + /// + public async Task CreateReactionAsync(ulong channel_id, ulong message_id, string emoji) + => await this.ApiClient.CreateReactionAsync(channel_id, message_id, emoji); + + /// + /// Deletes own reaction + /// + /// Channel ID + /// Message ID + /// Emoji to remove from reaction + /// + public async Task DeleteOwnReactionAsync(ulong channel_id, ulong message_id, string emoji) + => await this.ApiClient.DeleteOwnReactionAsync(channel_id, message_id, emoji); + + /// + /// Deletes someone elses reaction + /// + /// Channel ID + /// Message ID + /// User ID + /// Emoji to remove + /// Reason why this reaction was removed + /// + public async Task DeleteUserReactionAsync(ulong channel_id, ulong message_id, ulong user_id, string emoji, string reason) + => await this.ApiClient.DeleteUserReactionAsync(channel_id, message_id, user_id, emoji, reason); + + /// + /// Gets all users that reacted with a specific emoji to a message + /// + /// Channel ID + /// Message ID + /// Emoji to check for + /// Whether to search for reactions after this message id. + /// The maximum amount of reactions to fetch. + /// + public async Task> GetReactionsAsync(ulong channel_id, ulong message_id, string emoji, ulong? after_id = null, int limit = 25) + => await this.ApiClient.GetReactionsAsync(channel_id, message_id, emoji, after_id, limit); + + /// + /// Gets all users that reacted with a specific emoji to a message + /// + /// Channel ID + /// Message ID + /// Emoji to check for + /// Whether to search for reactions after this message id. + /// The maximum amount of reactions to fetch. + /// + public async Task> GetReactionsAsync(ulong channel_id, ulong message_id, DiscordEmoji emoji, ulong? after_id = null, int limit = 25) + => await this.ApiClient.GetReactionsAsync(channel_id, message_id, emoji.ToReactionString(), after_id, limit); + + /// + /// Deletes all reactions from a message + /// + /// Channel ID + /// Message ID + /// Reason why all reactions were removed + /// + public async Task DeleteAllReactionsAsync(ulong channel_id, ulong message_id, string reason) + => await this.ApiClient.DeleteAllReactionsAsync(channel_id, message_id, reason); + + /// + /// Deletes all reactions of a specific reaction for a message. + /// + /// The ID of the channel. + /// The ID of the message. + /// The emoji to clear. + /// + public async Task DeleteReactionsEmojiAsync(ulong channelid, ulong messageId, string emoji) + => await this.ApiClient.DeleteReactionsEmojiAsync(channelid, messageId, emoji); + + #endregion + + #region Application Commands + /// + /// Gets all the global application commands for this application. + /// + /// A list of global application commands. + public async Task> GetGlobalApplicationCommandsAsync() => + await this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id); + + /// + /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. + /// + /// The list of commands to overwrite with. + /// The list of global commands. + public async Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => + await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); + + /// + /// Creates or overwrites a global application command. + /// + /// The command to create. + /// The created command. + public async Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => + await this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); + + /// + /// Gets a global application command by its ID. + /// + /// The ID of the command to get. + /// The command with the ID. + public async Task GetGlobalApplicationCommandAsync(ulong commandId) => + await this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); + + /// + /// Edits a global application command. + /// + /// The ID of the command to edit. + /// Action to perform. + /// The edited command. + public async Task EditGlobalApplicationCommandAsync(ulong commandId, Action action) + { + var mdl = new ApplicationCommandEditModel(); + action(mdl); + var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync()).Id; + return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); + } - return await this.ApiClient.EditOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken, builder, attachments); - } + /// + /// Deletes a global application command. + /// + /// The ID of the command to delete. + public async Task DeleteGlobalApplicationCommandAsync(ulong commandId) => + await this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); + + /// + /// Gets all the application commands for a guild. + /// + /// The ID of the guild to get application commands for. + /// A list of application commands in the guild. + public async Task> GetGuildApplicationCommandsAsync(ulong guildId) => + await this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId); + + /// + /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. + /// + /// The ID of the guild. + /// The list of commands to overwrite with. + /// The list of guild commands. + public async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => + await this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); + + /// + /// Creates or overwrites a guild application command. + /// + /// The ID of the guild to create the application command in. + /// The command to create. + /// The created command. + public async Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => + await this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); + + /// + /// Gets a application command in a guild by its ID. + /// + /// The ID of the guild the application command is in. + /// The ID of the command to get. + /// The command with the ID. + public async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => + await this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); + + /// + /// Edits a application command in a guild. + /// + /// The ID of the guild the application command is in. + /// The ID of the command to edit. + /// Action to perform. + /// The edited command. + public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, Action action) + { + var mdl = new ApplicationCommandEditModel(); + action(mdl); + var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync()).Id; + return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); + } - /// - /// Deletes the original interaction response. - /// The token of the interaction. - /// > - public Task DeleteOriginalInteractionResponseAsync(string interactionToken) => - this.ApiClient.DeleteOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken); - - /// - /// Creates a follow up message to an interaction. - /// - /// The token of the interaction. - /// The webhook builder. - /// The created. - public async Task CreateFollowupMessageAsync(string interactionToken, DiscordFollowupMessageBuilder builder) - { - builder.Validate(); + /// + /// Deletes a application command in a guild. + /// + /// The ID of the guild to delete the application command in. + /// The ID of the command. + public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => + await this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); + + /// + /// Creates a response to an interaction. + /// + /// The ID of the interaction. + /// The token of the interaction + /// The type of the response. + /// The data, if any, to send. + public async Task CreateInteractionResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, DiscordInteractionResponseBuilder builder = null) => + await this.ApiClient.CreateInteractionResponseAsync(interactionId, interactionToken, type, builder); + + /// + /// Gets the original interaction response. + /// + /// The original message that was sent. This does not work on ephemeral messages. + public async Task GetOriginalInteractionResponseAsync(string interactionToken) => + await this.ApiClient.GetOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken); + + /// + /// Edits the original interaction response. + /// + /// The token of the interaction. + /// The webhook builder. + /// Attached files to keep. + /// The edited. + public async Task EditOriginalInteractionResponseAsync(string interactionToken, DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(isInteractionResponse: true); - return await this.ApiClient.CreateFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, builder); - } + return await this.ApiClient.EditOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken, builder, attachments); + } - /// - /// Edits a follow up message. - /// - /// The token of the interaction. - /// The ID of the follow up message. - /// The webhook builder. - /// Attached files to keep. - /// The edited. - public async Task EditFollowupMessageAsync(string interactionToken, ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(isFollowup: true); + /// + /// Deletes the original interaction response. + /// The token of the interaction. + /// > + public async Task DeleteOriginalInteractionResponseAsync(string interactionToken) => + await this.ApiClient.DeleteOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken); + + /// + /// Creates a follow up message to an interaction. + /// + /// The token of the interaction. + /// The webhook builder. + /// The created. + public async Task CreateFollowupMessageAsync(string interactionToken, DiscordFollowupMessageBuilder builder) + { + builder.Validate(); - return await this.ApiClient.EditFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId, builder, attachments); - } + return await this.ApiClient.CreateFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, builder); + } - /// - /// Deletes a follow up message. - /// - /// The token of the interaction. - /// The ID of the follow up message. - public Task DeleteFollowupMessageAsync(string interactionToken, ulong messageId) => - this.ApiClient.DeleteFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId); - - /// - /// Gets all application command permissions in a guild. - /// - /// The guild ID. - /// A list of permissions. - public Task> GetGuildApplicationCommandsPermissionsAsync(ulong guildId) - => this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId); - - /// - /// Gets permissions for a application command in a guild. - /// - /// The guild ID. - /// The ID of the command to get them for. - /// The permissions. - public Task GetGuildApplicationCommandPermissionsAsync(ulong guildId, ulong commandId) - => this.ApiClient.GetApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId); - - /// - /// Edits permissions for a application command in a guild. - /// - /// The guild ID. - /// The ID of the command to edit permissions for. - /// The list of permissions to use. - /// The edited permissions. - public Task EditApplicationCommandPermissionsAsync(ulong guildId, ulong commandId, IEnumerable permissions) - => this.ApiClient.EditApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId, permissions); - - /// - /// Batch edits permissions for a application command in a guild. - /// - /// The guild ID. - /// The list of permissions to use. - /// A list of edited permissions. - public Task> BatchEditApplicationCommandPermissionsAsync(ulong guildId, IEnumerable permissions) - => this.ApiClient.BatchEditApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, permissions); - - public Task GetFollowupMessageAsync(string interactionToken, ulong messageId) - => this.ApiClient.GetFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId); - - #endregion - - #region Stickers - - /// - /// Gets a sticker from a guild. - /// - /// The ID of the guild. - /// The ID of the sticker. - public Task GetGuildStickerAsync(ulong guildId, ulong stickerId) - => this.ApiClient.GetGuildStickerAsync(guildId, stickerId); - - /// - /// Gets a sticker by its ID. - /// - /// The ID of the sticker. - public Task GetStickerAsync(ulong stickerId) - => this.ApiClient.GetStickerAsync(stickerId); - - /// - /// Gets a collection of sticker packs that may be used by nitro users. - /// - public Task> GetStickerPacksAsync() - => this.ApiClient.GetStickerPacksAsync(); - - /// - /// Gets a list of stickers from a guild. - /// - /// The ID of the guild. - public Task> GetGuildStickersAsync(ulong guildId) - => this.ApiClient.GetGuildStickersAsync(guildId); - - /// - /// Creates a sticker in a guild. - /// - /// The ID of the guild. - /// The name of the sticker. - /// The description of the sticker. - /// The tags of the sticker. - /// The image content of the sticker. - /// The image format of the sticker. - /// The reason this sticker is being created. - - public Task CreateGuildStickerAsync(ulong guildId, string name, string description, string tags, Stream imageContents, StickerFormat format, string reason = null) - { - string contentType = null, extension = null; + /// + /// Edits a follow up message. + /// + /// The token of the interaction. + /// The ID of the follow up message. + /// The webhook builder. + /// Attached files to keep. + /// The edited. + public async Task EditFollowupMessageAsync(string interactionToken, ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(isFollowup: true); - if (format == StickerFormat.PNG || format == StickerFormat.APNG) - { - contentType = "image/png"; - extension = "png"; - } - else - { - contentType = "application/json"; - extension = "json"; - } + return await this.ApiClient.EditFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId, builder, attachments); + } - return this.ApiClient.CreateGuildStickerAsync(guildId, name, description ?? string.Empty, tags, new DiscordMessageFile(null, imageContents, null, extension, contentType), reason); - } + /// + /// Deletes a follow up message. + /// + /// The token of the interaction. + /// The ID of the follow up message. + public async Task DeleteFollowupMessageAsync(string interactionToken, ulong messageId) => + await this.ApiClient.DeleteFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId); + + /// + /// Gets all application command permissions in a guild. + /// + /// The guild ID. + /// A list of permissions. + public async Task> GetGuildApplicationCommandsPermissionsAsync(ulong guildId) + => await this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId); + + /// + /// Gets permissions for a application command in a guild. + /// + /// The guild ID. + /// The ID of the command to get them for. + /// The permissions. + public async Task GetGuildApplicationCommandPermissionsAsync(ulong guildId, ulong commandId) + => await this.ApiClient.GetApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId); + + /// + /// Edits permissions for a application command in a guild. + /// + /// The guild ID. + /// The ID of the command to edit permissions for. + /// The list of permissions to use. + /// The edited permissions. + public async Task EditApplicationCommandPermissionsAsync(ulong guildId, ulong commandId, IEnumerable permissions) + => await this.ApiClient.EditApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId, permissions); + + /// + /// Batch edits permissions for a application command in a guild. + /// + /// The guild ID. + /// The list of permissions to use. + /// A list of edited permissions. + public async Task> BatchEditApplicationCommandPermissionsAsync(ulong guildId, IEnumerable permissions) + => await this.ApiClient.BatchEditApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, permissions); + + public async Task GetFollowupMessageAsync(string interactionToken, ulong messageId) + => await this.ApiClient.GetFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId); + + #endregion + + #region Stickers + + /// + /// Gets a sticker from a guild. + /// + /// The ID of the guild. + /// The ID of the sticker. + public async Task GetGuildStickerAsync(ulong guildId, ulong stickerId) + => await this.ApiClient.GetGuildStickerAsync(guildId, stickerId); + + /// + /// Gets a sticker by its ID. + /// + /// The ID of the sticker. + public async Task GetStickerAsync(ulong stickerId) + => await this.ApiClient.GetStickerAsync(stickerId); + + /// + /// Gets a collection of sticker packs that may be used by nitro users. + /// + public async Task> GetStickerPacksAsync() + => await this.ApiClient.GetStickerPacksAsync(); + + /// + /// Gets a list of stickers from a guild. + /// + /// The ID of the guild. + public async Task> GetGuildStickersAsync(ulong guildId) + => await this.ApiClient.GetGuildStickersAsync(guildId); + + /// + /// Creates a sticker in a guild. + /// + /// The ID of the guild. + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The image content of the sticker. + /// The image format of the sticker. + /// The reason this sticker is being created. + + public async Task CreateGuildStickerAsync(ulong guildId, string name, string description, string tags, Stream imageContents, StickerFormat format, string reason = null) + { + string contentType = null, extension = null; - /// - /// Modifies a sticker in a guild. - /// - /// The ID of the guild. - /// The ID of the sticker. - /// Action to perform. - /// Reason for audit log. - public Task ModifyGuildStickerAsync(ulong guildId, ulong stickerId, Action action, string reason = null) + if (format == StickerFormat.PNG || format == StickerFormat.APNG) { - var mdl = new StickerEditModel(); - action(mdl); - return this.ApiClient.ModifyStickerAsync(guildId, stickerId, mdl.Name, mdl.Description, mdl.Tags, reason); + contentType = "image/png"; + extension = "png"; } - - /// - /// Deletes a sticker in a guild. - /// - /// The ID of the guild. - /// The ID of the sticker. - /// Reason for audit log. - /// - public Task DeleteGuildStickerAsync(ulong guildId, ulong stickerId, string reason = null) - => this.ApiClient.DeleteStickerAsync(guildId, stickerId, reason); - - #endregion - - #region Threads - - /// - /// Creates a thread from a message. - /// - /// The ID of the channel. - /// The ID of the message - /// The name of the thread. - /// The auto archive duration. - /// Reason for audit logs. - public Task CreateThreadFromMessageAsync(ulong channelId, ulong messageId, string name, AutoArchiveDuration archiveAfter, string reason = null) - => this.ApiClient.CreateThreadFromMessageAsync(channelId, messageId, name, archiveAfter, reason); - - /// - /// Creates a thread. - /// - /// The ID of the channel. - /// The name of the thread. - /// The auto archive duration. - /// The type of the thread. - /// Reason for audit logs. - /// - public Task CreateThreadAsync(ulong channelId, string name, AutoArchiveDuration archiveAfter, ChannelType threadType, string reason = null) - => this.ApiClient.CreateThreadAsync(channelId, name, archiveAfter, threadType, reason); - - /// - /// Joins a thread. - /// - /// The ID of the thread. - public Task JoinThreadAsync(ulong threadId) - => this.ApiClient.JoinThreadAsync(threadId); - - /// - /// Leaves a thread. - /// - /// The ID of the thread. - public Task LeaveThreadAsync(ulong threadId) - => this.ApiClient.LeaveThreadAsync(threadId); - - /// - /// Adds a member to a thread. - /// - /// The ID of the thread. - /// The ID of the member. - public Task AddThreadMemberAsync(ulong threadId, ulong userId) - => this.ApiClient.AddThreadMemberAsync(threadId, userId); - - /// - /// Removes a member from a thread. - /// - /// The ID of the thread. - /// The ID of the member. - public Task RemoveThreadMemberAsync(ulong threadId, ulong userId) - => this.ApiClient.RemoveThreadMemberAsync(threadId, userId); - - /// - /// Lists the members of a thread. - /// - /// The ID of the thread. - public Task> ListThreadMembersAsync(ulong threadId) - => this.ApiClient.ListThreadMembersAsync(threadId); - - /// - /// Lists the active threads of a guild. - /// - /// The ID of the guild. - public Task ListActiveThreadAsync(ulong guildId) - => this.ApiClient.ListActiveThreadsAsync(guildId); - - /// - /// Gets the threads that are public and archived for a channel. - /// - /// The ID of the guild. - /// The ID of the channel. - /// Date to filter by. - /// Limit. - public Task ListPublicArchivedThreadsAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) - => this.ApiClient.ListPublicArchivedThreadsAsync(guildId, channelId, before?.ToString("o"), limit); - - /// - /// Gets the threads that are public and archived for a channel. - /// - /// The ID of the guild. - /// The ID of the channel. - /// Date to filter by. - /// Limit. - public Task ListPrivateArchivedThreadAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) - => this.ApiClient.ListPrivateArchivedThreadsAsync(guildId, channelId, before?.ToString("o"), limit); - - /// - /// Gets the private archived threads the user has joined for a channel. - /// - /// The ID of the guild. - /// The ID of the channel. - /// Date to filter by. - /// Limit. - public Task ListJoinedPrivateArchivedThreadsAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) - => this.ApiClient.ListJoinedPrivateArchivedThreadsAsync(guildId, channelId, (ulong?)before?.ToUnixTimeSeconds(), limit); - - #endregion - - #region Emoji - - /// - /// Gets a guild's emojis. - /// - /// The ID of the guild. - public Task> GetGuildEmojisAsync(ulong guildId) - => this.ApiClient.GetGuildEmojisAsync(guildId); - - /// - /// Gets a guild emoji. - /// - /// The ID of the guild. - /// The ID of the emoji. - public Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) - => this.ApiClient.GetGuildEmojiAsync(guildId, emojiId); - - /// - /// Creates an emoji in a guild. - /// - /// Name of the emoji. - /// The ID of the guild. - /// Image to use as the emoji. - /// Roles for which the emoji will be available. - /// Reason for audit logs. - public Task CreateEmojiAsync(ulong guildId, string name, Stream image, IEnumerable roles = null, string reason = null) + else { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullException(nameof(name)); + contentType = "application/json"; + extension = "json"; + } + + return await this.ApiClient.CreateGuildStickerAsync(guildId, name, description ?? string.Empty, tags, new DiscordMessageFile(null, imageContents, null, extension, contentType), reason); + } + + /// + /// Modifies a sticker in a guild. + /// + /// The ID of the guild. + /// The ID of the sticker. + /// Action to perform. + /// Reason for audit log. + public async Task ModifyGuildStickerAsync(ulong guildId, ulong stickerId, Action action, string reason = null) + { + var mdl = new StickerEditModel(); + action(mdl); + return await this.ApiClient.ModifyStickerAsync(guildId, stickerId, mdl.Name, mdl.Description, mdl.Tags, reason); + } - name = name.Trim(); - if (name.Length < 2 || name.Length > 50) - throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); + /// + /// Deletes a sticker in a guild. + /// + /// The ID of the guild. + /// The ID of the sticker. + /// Reason for audit log. + /// + public async Task DeleteGuildStickerAsync(ulong guildId, ulong stickerId, string reason = null) + => await this.ApiClient.DeleteStickerAsync(guildId, stickerId, reason); + + #endregion + + #region Threads + + /// + /// Creates a thread from a message. + /// + /// The ID of the channel. + /// The ID of the message + /// The name of the thread. + /// The auto archive duration. + /// Reason for audit logs. + public async Task CreateThreadFromMessageAsync(ulong channelId, ulong messageId, string name, AutoArchiveDuration archiveAfter, string reason = null) + => await this.ApiClient.CreateThreadFromMessageAsync(channelId, messageId, name, archiveAfter, reason); + + /// + /// Creates a thread. + /// + /// The ID of the channel. + /// The name of the thread. + /// The auto archive duration. + /// The type of the thread. + /// Reason for audit logs. + /// + public async Task CreateThreadAsync(ulong channelId, string name, AutoArchiveDuration archiveAfter, ChannelType threadType, string reason = null) + => await this.ApiClient.CreateThreadAsync(channelId, name, archiveAfter, threadType, reason); + + /// + /// Joins a thread. + /// + /// The ID of the thread. + public async Task JoinThreadAsync(ulong threadId) + => await this.ApiClient.JoinThreadAsync(threadId); + + /// + /// Leaves a thread. + /// + /// The ID of the thread. + public async Task LeaveThreadAsync(ulong threadId) + => await this.ApiClient.LeaveThreadAsync(threadId); + + /// + /// Adds a member to a thread. + /// + /// The ID of the thread. + /// The ID of the member. + public async Task AddThreadMemberAsync(ulong threadId, ulong userId) + => await this.ApiClient.AddThreadMemberAsync(threadId, userId); + + /// + /// Removes a member from a thread. + /// + /// The ID of the thread. + /// The ID of the member. + public async Task RemoveThreadMemberAsync(ulong threadId, ulong userId) + => await this.ApiClient.RemoveThreadMemberAsync(threadId, userId); + + /// + /// Lists the members of a thread. + /// + /// The ID of the thread. + public async Task> ListThreadMembersAsync(ulong threadId) + => await this.ApiClient.ListThreadMembersAsync(threadId); + + /// + /// Lists the active threads of a guild. + /// + /// The ID of the guild. + public async Task ListActiveThreadAsync(ulong guildId) + => await this.ApiClient.ListActiveThreadsAsync(guildId); + + /// + /// Gets the threads that are public and archived for a channel. + /// + /// The ID of the guild. + /// The ID of the channel. + /// Date to filter by. + /// Limit. + public async Task ListPublicArchivedThreadsAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) + => await this.ApiClient.ListPublicArchivedThreadsAsync(guildId, channelId, before?.ToString("o"), limit); + + /// + /// Gets the threads that are public and archived for a channel. + /// + /// The ID of the guild. + /// The ID of the channel. + /// Date to filter by. + /// Limit. + public async Task ListPrivateArchivedThreadAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) + => await this.ApiClient.ListPrivateArchivedThreadsAsync(guildId, channelId, limit, before?.ToString("o")); + + /// + /// Gets the private archived threads the user has joined for a channel. + /// + /// The ID of the guild. + /// The ID of the channel. + /// Date to filter by. + /// Limit. + public async Task ListJoinedPrivateArchivedThreadsAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) + => await this.ApiClient.ListJoinedPrivateArchivedThreadsAsync(guildId, channelId, limit, (ulong?)before?.ToUnixTimeSeconds()); + + #endregion + + #region Emoji + + /// + /// Gets a guild's emojis. + /// + /// The ID of the guild. + public async Task> GetGuildEmojisAsync(ulong guildId) + => await this.ApiClient.GetGuildEmojisAsync(guildId); + + /// + /// Gets a guild emoji. + /// + /// The ID of the guild. + /// The ID of the emoji. + public async Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) + => await this.ApiClient.GetGuildEmojiAsync(guildId, emojiId); + + /// + /// Creates an emoji in a guild. + /// + /// Name of the emoji. + /// The ID of the guild. + /// Image to use as the emoji. + /// Roles for which the emoji will be available. + /// Reason for audit logs. + public async Task CreateEmojiAsync(ulong guildId, string name, Stream image, IEnumerable roles = null, string reason = null) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); - if (image == null) - throw new ArgumentNullException(nameof(image)); + name = name.Trim(); + if (name.Length < 2 || name.Length > 50) + throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); - string image64 = null; - using (var imgtool = new ImageTool(image)) - image64 = imgtool.GetBase64(); + if (image == null) + throw new ArgumentNullException(nameof(image)); - return this.ApiClient.CreateGuildEmojiAsync(guildId, name, image64, roles, reason); - } + string image64 = null; + using (var imgtool = new ImageTool(image)) + image64 = imgtool.GetBase64(); - /// - /// Modifies a guild's emoji. - /// - /// The ID of the guild. - /// The ID of the emoji. - /// New name of the emoji. - /// Roles for which the emoji will be available. - /// Reason for audit logs. - public Task ModifyGuildEmojiAsync(ulong guildId, ulong emojiId, string name, IEnumerable roles = null, string reason = null) - => this.ApiClient.ModifyGuildEmojiAsync(guildId, emojiId, name, roles, reason); - - /// - /// Deletes a guild's emoji. - /// - /// The ID of the guild. - /// The ID of the emoji. - /// Reason for audit logs. - public Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string reason = null) - => this.ApiClient.DeleteGuildEmojiAsync(guildId, emojiId, reason); - - #endregion - - #region Misc - /// - /// Gets assets from an application - /// - /// Application to get assets from - /// - public Task> GetApplicationAssetsAsync(DiscordApplication application) - => this.ApiClient.GetApplicationAssetsAsync(application); - - /// - /// Gets a guild template by the code. - /// - /// The code of the template. - /// The guild template for the code.\ - public Task GetTemplateAsync(string code) - => this.ApiClient.GetTemplateAsync(code); - #endregion - - private bool _disposed; - /// - /// Disposes of this DiscordRestClient - /// - public override void Dispose() - { - if (this._disposed) - return; - this._disposed = true; - this._guilds = null; - this.ApiClient?._rest?.Dispose(); - } + return await this.ApiClient.CreateGuildEmojiAsync(guildId, name, image64, roles, reason); + } + + /// + /// Modifies a guild's emoji. + /// + /// The ID of the guild. + /// The ID of the emoji. + /// New name of the emoji. + /// Roles for which the emoji will be available. + /// Reason for audit logs. + public async Task ModifyGuildEmojiAsync(ulong guildId, ulong emojiId, string name, IEnumerable roles = null, string reason = null) + => await this.ApiClient.ModifyGuildEmojiAsync(guildId, emojiId, name, roles, reason); + + /// + /// Deletes a guild's emoji. + /// + /// The ID of the guild. + /// The ID of the emoji. + /// Reason for audit logs. + public async Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string reason = null) + => await this.ApiClient.DeleteGuildEmojiAsync(guildId, emojiId, reason); + + #endregion + + #region Misc + /// + /// Gets assets from an application + /// + /// Application to get assets from + /// + public async Task> GetApplicationAssetsAsync(DiscordApplication application) + => await this.ApiClient.GetApplicationAssetsAsync(application); + + /// + /// Gets a guild template by the code. + /// + /// The code of the template. + /// The guild template for the code.\ + public async Task GetTemplateAsync(string code) + => await this.ApiClient.GetTemplateAsync(code); + #endregion + + private bool _disposed; + /// + /// Disposes of this DiscordRestClient + /// + public override void Dispose() + { + if (this._disposed) + return; + this._disposed = true; + this._guilds = null; + this.ApiClient?._rest?.Dispose(); } } diff --git a/DSharpPlus/Clients/BaseDiscordClient.cs b/DSharpPlus/Clients/BaseDiscordClient.cs index 5f97e437ca..a5e419be57 100644 --- a/DSharpPlus/Clients/BaseDiscordClient.cs +++ b/DSharpPlus/Clients/BaseDiscordClient.cs @@ -160,8 +160,8 @@ public async Task GetCurrentApplicationAsync() /// /// /// Thrown when Discord is unable to process the request. - public Task> ListVoiceRegionsAsync() - => this.ApiClient.ListVoiceRegionsAsync(); + public async Task> ListVoiceRegionsAsync() + => await this.ApiClient.ListVoiceRegionsAsync(); /// /// Initializes this client. This method fetches information about current user, application, and voice regions. diff --git a/DSharpPlus/Clients/DiscordClient.WebSocket.cs b/DSharpPlus/Clients/DiscordClient.WebSocket.cs index f0055db941..4527b7cf23 100644 --- a/DSharpPlus/Clients/DiscordClient.WebSocket.cs +++ b/DSharpPlus/Clients/DiscordClient.WebSocket.cs @@ -113,14 +113,16 @@ internal async Task InternalConnectAsync() this._webSocketClient.MessageReceived += SocketOnMessage; this._webSocketClient.ExceptionThrown += SocketOnException; - var gwuri = new QueryUriBuilder(this.GatewayUri) + QueryUriBuilder gwuri = new QueryUriBuilder(this.GatewayUri.ToString()) .AddParameter("v", "10") .AddParameter("encoding", "json"); if (this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Stream) + { gwuri.AddParameter("compress", "zlib-stream"); + } - await this._webSocketClient.ConnectAsync(gwuri.Build()); + await this._webSocketClient.ConnectAsync(new Uri(gwuri.Build())); Task SocketOnConnect(IWebSocketClient sender, SocketEventArgs e) => this._socketOpened.InvokeAsync(this, e); diff --git a/DSharpPlus/Clients/DiscordClient.cs b/DSharpPlus/Clients/DiscordClient.cs index 5935e5f7ab..c94da8ec3d 100644 --- a/DSharpPlus/Clients/DiscordClient.cs +++ b/DSharpPlus/Clients/DiscordClient.cs @@ -355,15 +355,15 @@ public async Task DisconnectAsync() /// /// The ID of the sticker. /// The specified sticker - public Task GetStickerAsync(ulong stickerId) - => this.ApiClient.GetStickerAsync(stickerId); + public async Task GetStickerAsync(ulong stickerId) + => await this.ApiClient.GetStickerAsync(stickerId); /// /// Gets a collection of sticker packs that may be used by nitro users. /// /// - public Task> GetStickerPacksAsync() - => this.ApiClient.GetStickerPacksAsync(); + public async Task> GetStickerPacksAsync() + => await this.ApiClient.GetStickerPacksAsync(); /// /// Gets a user @@ -407,8 +407,8 @@ public async Task GetChannelAsync(ulong id) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(DiscordChannel channel, string content) - => this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + public async Task SendMessageAsync(DiscordChannel channel, string content) + => await this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Sends a message @@ -420,8 +420,8 @@ public Task SendMessageAsync(DiscordChannel channel, string cont /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) - => this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + public async Task SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) + => await this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Sends a message @@ -434,8 +434,8 @@ public Task SendMessageAsync(DiscordChannel channel, DiscordEmbe /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) - => this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + public async Task SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) + => await this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Sends a message @@ -447,8 +447,8 @@ public Task SendMessageAsync(DiscordChannel channel, string cont /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(DiscordChannel channel, DiscordMessageBuilder builder) - => this.ApiClient.CreateMessageAsync(channel.Id, builder); + public async Task SendMessageAsync(DiscordChannel channel, DiscordMessageBuilder builder) + => await this.ApiClient.CreateMessageAsync(channel.Id, builder); /// /// Sends a message @@ -460,12 +460,12 @@ public Task SendMessageAsync(DiscordChannel channel, DiscordMess /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(DiscordChannel channel, Action action) + public async Task SendMessageAsync(DiscordChannel channel, Action action) { - var builder = new DiscordMessageBuilder(); + DiscordMessageBuilder builder = new DiscordMessageBuilder(); action(builder); - return this.ApiClient.CreateMessageAsync(channel.Id, builder); + return await this.ApiClient.CreateMessageAsync(channel.Id, builder); } /// @@ -481,7 +481,7 @@ public Task SendMessageAsync(DiscordChannel channel, ActionThrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task CreateGuildAsync(string name, string region = null, Optional icon = default, VerificationLevel? verificationLevel = null, + public async Task CreateGuildAsync(string name, string region = null, Optional icon = default, VerificationLevel? verificationLevel = null, DefaultMessageNotifications? defaultMessageNotifications = null, SystemChannelFlags? systemChannelFlags = null) { @@ -492,7 +492,7 @@ public Task CreateGuildAsync(string name, string region = null, Op else if (icon.HasValue) iconb64 = null; - return this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, defaultMessageNotifications, systemChannelFlags); + return await this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, defaultMessageNotifications, systemChannelFlags); } /// @@ -504,7 +504,7 @@ public Task CreateGuildAsync(string name, string region = null, Op /// The created guild. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) + public async Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) { var iconb64 = Optional.FromNoValue(); if (icon.HasValue && icon.Value != null) @@ -513,7 +513,7 @@ public Task CreateGuildFromTemplateAsync(string code, string name, else if (icon.HasValue) iconb64 = null; - return this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); + return await this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); } /// @@ -546,8 +546,8 @@ public async Task GetGuildAsync(ulong id, bool? withCounts = null) /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task GetGuildPreviewAsync(ulong id) - => this.ApiClient.GetGuildPreviewAsync(id); + public async Task GetGuildPreviewAsync(ulong id) + => await this.ApiClient.GetGuildPreviewAsync(id); /// /// Gets an invite. @@ -559,8 +559,8 @@ public Task GetGuildPreviewAsync(ulong id) /// Thrown when the invite does not exists. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null) - => this.ApiClient.GetInviteAsync(code, withCounts, withExpiration); + public async Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null) + => await this.ApiClient.GetInviteAsync(code, withCounts, withExpiration); /// /// Gets a list of connections @@ -568,8 +568,8 @@ public Task GetInviteByCodeAsync(string code, bool? withCounts = /// /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task> GetConnectionsAsync() - => this.ApiClient.GetUsersConnectionsAsync(); + public async Task> GetConnectionsAsync() + => await this.ApiClient.GetUsersConnectionsAsync(); /// /// Gets a webhook @@ -579,8 +579,8 @@ public Task> GetConnectionsAsync() /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task GetWebhookAsync(ulong id) - => this.ApiClient.GetWebhookAsync(id); + public async Task GetWebhookAsync(ulong id) + => await this.ApiClient.GetWebhookAsync(id); /// /// Gets a webhook @@ -591,8 +591,8 @@ public Task GetWebhookAsync(ulong id) /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task GetWebhookWithTokenAsync(ulong id, string token) - => this.ApiClient.GetWebhookWithTokenAsync(id, token); + public async Task GetWebhookWithTokenAsync(ulong id, string token) + => await this.ApiClient.GetWebhookWithTokenAsync(id, token); /// /// Updates current user's activity and status. @@ -637,39 +637,39 @@ public async Task UpdateCurrentUserAsync(string username = null, Op /// The guild template for the code. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task GetTemplateAsync(string code) - => this.ApiClient.GetTemplateAsync(code); + public async Task GetTemplateAsync(string code) + => await this.ApiClient.GetTemplateAsync(code); /// /// Gets all the global application commands for this application. /// /// A list of global application commands. - public Task> GetGlobalApplicationCommandsAsync() => - this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id); + public async Task> GetGlobalApplicationCommandsAsync() => + await this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id); /// /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. /// /// The list of commands to overwrite with. /// The list of global commands. - public Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => - this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); + public async Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => + await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); /// /// Creates or overwrites a global application command. /// /// The command to create. /// The created command. - public Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => - this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); + public async Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => + await this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); /// /// Gets a global application command by its id. /// /// The ID of the command to get. /// The command with the ID. - public Task GetGlobalApplicationCommandAsync(ulong commandId) => - this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); + public async Task GetGlobalApplicationCommandAsync(ulong commandId) => + await this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Gets a global application command by its name. @@ -703,16 +703,16 @@ public async Task EditGlobalApplicationCommandAsync(u /// Deletes a global application command. /// /// The ID of the command to delete. - public Task DeleteGlobalApplicationCommandAsync(ulong commandId) => - this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); + public async Task DeleteGlobalApplicationCommandAsync(ulong commandId) => + await this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Gets all the application commands for a guild. /// /// The ID of the guild to get application commands for. /// A list of application commands in the guild. - public Task> GetGuildApplicationCommandsAsync(ulong guildId) => - this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId); + public async Task> GetGuildApplicationCommandsAsync(ulong guildId) => + await this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId); /// /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. @@ -720,8 +720,8 @@ public Task> GetGuildApplicationCommand /// The ID of the guild. /// The list of commands to overwrite with. /// The list of guild commands. - public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => - this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); + public async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => + await this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); /// /// Creates or overwrites a guild application command. @@ -729,8 +729,8 @@ public Task> BulkOverwriteGuildApplicat /// The ID of the guild to create the application command in. /// The command to create. /// The created command. - public Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => - this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); + public async Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => + await this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); /// /// Gets a application command in a guild by its ID. @@ -738,8 +738,8 @@ public Task CreateGuildApplicationCommandAsync(ulong /// The ID of the guild the application command is in. /// The ID of the command to get. /// The command with the ID. - public Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); + public async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => + await this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Edits a application command in a guild. @@ -761,8 +761,8 @@ public async Task EditGuildApplicationCommandAsync(ul /// /// The ID of the guild to delete the application command in. /// The ID of the command. - public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); + public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => + await this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); #endregion #region Internal Caching Methods diff --git a/DSharpPlus/Clients/DiscordWebhookClient.cs b/DSharpPlus/Clients/DiscordWebhookClient.cs index 46333da575..f5c0016a5d 100644 --- a/DSharpPlus/Clients/DiscordWebhookClient.cs +++ b/DSharpPlus/Clients/DiscordWebhookClient.cs @@ -79,7 +79,7 @@ public DiscordWebhookClient(IWebProxy proxy = null, TimeSpan? timeout = null, bo var parsedTimeout = timeout ?? TimeSpan.FromSeconds(10); - this._apiclient = new DiscordApiClient(proxy, parsedTimeout, useRelativeRateLimit, this.Logger); + this._apiclient = new DiscordApiClient(proxy, parsedTimeout, this.Logger); this._hooks = new List(); this.Webhooks = new ReadOnlyCollection(this._hooks); } diff --git a/DSharpPlus/DSharpPlus.csproj b/DSharpPlus/DSharpPlus.csproj index a561b8d662..1d0ccd43c9 100644 --- a/DSharpPlus/DSharpPlus.csproj +++ b/DSharpPlus/DSharpPlus.csproj @@ -7,7 +7,8 @@ - + + \ No newline at end of file diff --git a/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs b/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs index 466d852fe8..f414d26185 100644 --- a/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs +++ b/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs @@ -90,8 +90,8 @@ public class DiscordAutoModerationRule : SnowflakeObject /// Deletes the rule in the guild. /// /// Reason for audits logs. - public Task DeleteAsync(string reason = null) - => this.Discord.ApiClient.DeleteGuildAutoModerationRuleAsync(this.GuildId, this.Id, reason); + public async Task DeleteAsync(string reason = null) + => await this.Discord.ApiClient.DeleteGuildAutoModerationRuleAsync(this.GuildId, this.Id, reason); /// /// Modify the rule in the guild. diff --git a/DSharpPlus/Entities/Channel/DiscordChannel.cs b/DSharpPlus/Entities/Channel/DiscordChannel.cs index a7df3ce9cf..8e87279aed 100644 --- a/DSharpPlus/Entities/Channel/DiscordChannel.cs +++ b/DSharpPlus/Entities/Channel/DiscordChannel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Collections.ObjectModel; using System.IO; using System.Linq; @@ -232,12 +231,11 @@ internal DiscordChannel() /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(string content) + public async Task SendMessageAsync(string content) { - if (!Utilities.IsTextableChannel(this)) //NOTE: InvalidOperationException would be more apt, but would be a breaking change. - throw new ArgumentException($"{this.Type} channels do not support sending text messages."); - - return this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + return !Utilities.IsTextableChannel(this) + ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") + : await this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); } /// @@ -249,12 +247,11 @@ public Task SendMessageAsync(string content) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(DiscordEmbed embed) + public async Task SendMessageAsync(DiscordEmbed embed) { - if (!Utilities.IsTextableChannel(this)) - throw new ArgumentException($"{this.Type} channels do not support sending text messages."); - - return this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + return !Utilities.IsTextableChannel(this) + ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") + : await this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); } /// @@ -267,12 +264,11 @@ public Task SendMessageAsync(DiscordEmbed embed) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(string content, DiscordEmbed embed) + public async Task SendMessageAsync(string content, DiscordEmbed embed) { - if (!Utilities.IsTextableChannel(this)) - throw new ArgumentException($"{this.Type} channels do not support sending text messages."); - - return this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + return !Utilities.IsTextableChannel(this) + ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") + : await this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); } /// @@ -284,12 +280,11 @@ public Task SendMessageAsync(string content, DiscordEmbed embed) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(DiscordMessageBuilder builder) + public async Task SendMessageAsync(DiscordMessageBuilder builder) { - if (!Utilities.IsTextableChannel(this)) - throw new ArgumentException($"{this.Type} channels do not support sending text messages."); - - return this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); + return !Utilities.IsTextableChannel(this) + ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") + : await this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); } /// @@ -301,16 +296,18 @@ public Task SendMessageAsync(DiscordMessageBuilder builder) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SendMessageAsync(Action action) + public async Task SendMessageAsync(Action action) { if (!Utilities.IsTextableChannel(this)) + { throw new ArgumentException($"{this.Type} channels do not support sending text messages."); + } - var builder = new DiscordMessageBuilder(); + DiscordMessageBuilder builder = new DiscordMessageBuilder(); action(builder); - return this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); + return await this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); } /// @@ -338,8 +335,8 @@ public Task CreateGuildEventAsync(string name, strin /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteAsync(string reason = null) - => this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason); + public async Task DeleteAsync(string reason = null) + => await this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason); /// /// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages. @@ -407,11 +404,11 @@ public async Task GetMessageAsync(ulong id, bool skipCache = fal /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(Action action) + public async Task ModifyAsync(Action action) { - var mdl = new ChannelEditModel(); + ChannelEditModel mdl = new ChannelEditModel(); action(mdl); - return this.Discord.ApiClient.ModifyChannelAsync + await this.Discord.ApiClient.ModifyChannelAsync ( this.Id, mdl.Name, @@ -426,14 +423,14 @@ public Task ModifyAsync(Action action) mdl.QualityMode, mdl.Type, mdl.PermissionOverwrites, - mdl.AuditLogReason, mdl.Flags, mdl.AvailableTags, mdl.DefaultAutoArchiveDuration, mdl.DefaultReaction, mdl.DefaultThreadRateLimit, mdl.DefaultSortOrder, - mdl.DefaultForumLayout + mdl.DefaultForumLayout, + mdl.AuditLogReason ); } @@ -449,26 +446,27 @@ public Task ModifyAsync(Action action) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyPositionAsync(int position, string reason = null, bool? lockPermissions = null, ulong? parentId = null) + public async Task ModifyPositionAsync(int position, string reason = null, bool? lockPermissions = null, ulong? parentId = null) { - if (this.Guild == null) + if (this.Guild is null) + { throw new InvalidOperationException("Cannot modify order of non-guild channels."); + } - var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray(); - var pmds = new RestGuildChannelReorderPayload[chns.Length]; - for (var i = 0; i < chns.Length; i++) + DiscordChannel[] chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray(); + RestGuildChannelReorderPayload[] pmds = new RestGuildChannelReorderPayload[chns.Length]; + for (int i = 0; i < chns.Length; i++) { - pmds[i] = new RestGuildChannelReorderPayload + pmds[i] = new() { - ChannelId = chns[i].Id + ChannelId = chns[i].Id, + Position = chns[i].Id == this.Id ? position : chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position, + LockPermissions = chns[i].Id == this.Id ? lockPermissions : null, + ParentId = chns[i].Id == this.Id ? parentId : null }; - - pmds[i].Position = chns[i].Id == this.Id ? position : chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position; - pmds[i].LockPermissions = chns[i].Id == this.Id ? lockPermissions : null; - pmds[i].ParentId = chns[i].Id == this.Id ? parentId : null; } - return this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); + await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); } /// @@ -571,12 +569,11 @@ private async Task> GetMessagesInternalAsync(int l /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ListPublicArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) + public async Task ListPublicArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) { - if (this.Type != ChannelType.Text && this.Type != ChannelType.News) - throw new InvalidOperationException(); - - return this.Discord.ApiClient.ListPublicArchivedThreadsAsync(this.GuildId.Value, this.Id, before?.ToString("o"), limit); + return this.Type != ChannelType.Text && this.Type != ChannelType.News + ? throw new InvalidOperationException() + : await this.Discord.ApiClient.ListPublicArchivedThreadsAsync(this.GuildId.Value, this.Id, before?.ToString("o"), limit); } /// @@ -587,12 +584,11 @@ public Task ListPublicArchivedThreadsAsync(DateTimeOffset? be /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ListPrivateArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) + public async Task ListPrivateArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) { - if (this.Type != ChannelType.Text && this.Type != ChannelType.News) - throw new InvalidOperationException(); - - return this.Discord.ApiClient.ListPrivateArchivedThreadsAsync(this.GuildId.Value, this.Id, before?.ToString("o"), limit); + return this.Type != ChannelType.Text && this.Type != ChannelType.News + ? throw new InvalidOperationException() + : await this.Discord.ApiClient.ListPrivateArchivedThreadsAsync(this.GuildId.Value, this.Id, limit, before?.ToString("o")); } /// @@ -603,12 +599,11 @@ public Task ListPrivateArchivedThreadsAsync(DateTimeOffset? b /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ListJoinedPrivateArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) + public async Task ListJoinedPrivateArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) { - if (this.Type != ChannelType.Text && this.Type != ChannelType.News) - throw new InvalidOperationException(); - - return this.Discord.ApiClient.ListJoinedPrivateArchivedThreadsAsync(this.GuildId.Value, this.Id, (ulong?)before?.ToUnixTimeSeconds(), limit); + return this.Type != ChannelType.Text && this.Type != ChannelType.News + ? throw new InvalidOperationException() + : await this.Discord.ApiClient.ListJoinedPrivateArchivedThreadsAsync(this.GuildId.Value, this.Id, limit, (ulong?)before?.ToUnixTimeSeconds()); } /// @@ -648,8 +643,8 @@ public async Task DeleteMessagesAsync(IEnumerable messages, stri /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteMessageAsync(DiscordMessage message, string reason = null) - => this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); + public async Task DeleteMessageAsync(DiscordMessage message, string reason = null) + => await this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); /// /// Returns a list of invite objects @@ -659,11 +654,11 @@ public Task DeleteMessageAsync(DiscordMessage message, string reason = null) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task> GetInvitesAsync() + public async Task> GetInvitesAsync() { return this.Guild == null ? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.") - : this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); + : await this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); } /// @@ -682,8 +677,8 @@ public Task> GetInvitesAsync() /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, string reason = null, InviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) - => this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); + public async Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, string reason = null, InviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) + => await this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); /// /// Adds a channel permission overwrite for specified member. @@ -697,8 +692,8 @@ public Task CreateInviteAsync(int max_age = 86400, int max_uses = /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) - => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason); + public async Task AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) + => await this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason); /// /// Adds a channel permission overwrite for specified role. @@ -712,8 +707,8 @@ public Task AddOverwriteAsync(DiscordMember member, Permissions allow = Permissi /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) - => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason); + public async Task AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) + => await this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason); /// /// Deletes a channel permission overwrite for the specified member. @@ -725,8 +720,8 @@ public Task AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteOverwriteAsync(DiscordMember member, string reason = null) - => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason); + public async Task DeleteOverwriteAsync(DiscordMember member, string reason = null) + => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason); /// /// Deletes a channel permission overwrite for the specified role. @@ -738,8 +733,8 @@ public Task DeleteOverwriteAsync(DiscordMember member, string reason = null) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteOverwriteAsync(DiscordRole role, string reason = null) - => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason); + public async Task DeleteOverwriteAsync(DiscordRole role, string reason = null) + => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason); /// /// Post a typing indicator @@ -748,11 +743,16 @@ public Task DeleteOverwriteAsync(DiscordRole role, string reason = null) /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task TriggerTypingAsync() + public async Task TriggerTypingAsync() { - return !Utilities.IsTextableChannel(this) - ? throw new ArgumentException("Cannot start typing in a non-text channel.") - : this.Discord.ApiClient.TriggerTypingAsync(this.Id); + if (!Utilities.IsTextableChannel(this)) + { + throw new ArgumentException("Cannot start typing in a non-text channel."); + } + else + { + await this.Discord.ApiClient.TriggerTypingAsync(this.Id); + } } /// @@ -763,11 +763,11 @@ public Task TriggerTypingAsync() /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task> GetPinnedMessagesAsync() + public async Task> GetPinnedMessagesAsync() { return !Utilities.IsTextableChannel(this) || this.Type is ChannelType.Voice ? throw new ArgumentException("A non-text channel does not have pinned messages.") - : this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); + : await this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); } /// @@ -800,8 +800,8 @@ public async Task CreateWebhookAsync(string name, OptionalThrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when Discord is unable to process the request. - public Task> GetWebhooksAsync() - => this.Discord.ApiClient.GetChannelWebhooksAsync(this.Id); + public async Task> GetWebhooksAsync() + => await this.Discord.ApiClient.GetChannelWebhooksAsync(this.Id); /// /// Moves a member to this voice channel @@ -827,11 +827,11 @@ await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, de /// Channel to crosspost messages to /// Thrown when trying to follow a non-news channel /// Thrown when the current user doesn't have on the target channel - public Task FollowAsync(DiscordChannel targetChannel) + public async Task FollowAsync(DiscordChannel targetChannel) { return this.Type != ChannelType.News ? throw new ArgumentException("Cannot follow a non-news channel.") - : this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id); + : await this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id); } /// @@ -842,11 +842,11 @@ public Task FollowAsync(DiscordChannel targetChannel) /// /// Thrown when the current user doesn't have and/or /// - public Task CrosspostMessageAsync(DiscordMessage message) + public async Task CrosspostMessageAsync(DiscordMessage message) { return (message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted ? throw new ArgumentException("Message is already crossposted.") - : this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id); + : await this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id); } /// @@ -858,7 +858,9 @@ public Task CrosspostMessageAsync(DiscordMessage message) public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) { if (this.Type != ChannelType.Stage) + { throw new ArgumentException("Voice state can only be updated in a stage channel."); + } await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp); } @@ -909,8 +911,8 @@ public async Task ModifyStageInstanceAsync(Action /// The reason the stage instance was deleted. - public Task DeleteStageInstanceAsync(string reason = null) - => this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason); + public async Task DeleteStageInstanceAsync(string reason = null) + => await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason); /// /// Calculates permissions for a given member. diff --git a/DSharpPlus/Entities/Channel/DiscordDmChannel.cs b/DSharpPlus/Entities/Channel/DiscordDmChannel.cs index fd28f52d65..a3b4afbff1 100644 --- a/DSharpPlus/Entities/Channel/DiscordDmChannel.cs +++ b/DSharpPlus/Entities/Channel/DiscordDmChannel.cs @@ -51,8 +51,8 @@ public string IconUrl /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task AddDmRecipientAsync(ulong user_id, string accesstoken, string nickname) - => this.Discord.ApiClient.AddGroupDmRecipientAsync(this.Id, user_id, accesstoken, nickname); + public async Task AddDmRecipientAsync(ulong user_id, string accesstoken, string nickname) + => await this.Discord.ApiClient.AddGroupDmRecipientAsync(this.Id, user_id, accesstoken, nickname); /// /// Only use for Group DMs! @@ -62,7 +62,7 @@ public Task AddDmRecipientAsync(ulong user_id, string accesstoken, string nickna /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task RemoveDmRecipientAsync(ulong user_id, string accesstoken) - => this.Discord.ApiClient.RemoveGroupDmRecipientAsync(this.Id, user_id); + public async Task RemoveDmRecipientAsync(ulong user_id, string accesstoken) + => await this.Discord.ApiClient.RemoveGroupDmRecipientAsync(this.Id, user_id); } } diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs index 27bfc56e0b..29584f3b81 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs @@ -409,8 +409,8 @@ internal void PopulateMentions() /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(Optional content) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, Array.Empty(), null, default); + public async Task ModifyAsync(Optional content) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, Array.Empty(), null, default); /// /// Edits the message. @@ -421,8 +421,8 @@ public Task ModifyAsync(Optional content) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(Optional embed = default) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.HasValue ? new[] { embed.Value } : Array.Empty(), this.GetMentions(), default, Array.Empty(), null, default); + public async Task ModifyAsync(Optional embed = default) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.HasValue ? new[] { embed.Value } : Array.Empty(), this.GetMentions(), default, Array.Empty(), null, default); /// /// Edits the message. @@ -434,8 +434,8 @@ public Task ModifyAsync(Optional embed = default) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(Optional content, Optional embed = default) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.HasValue ? new[] { embed.Value } : Array.Empty(), this.GetMentions(), default, Array.Empty(), null, default); + public async Task ModifyAsync(Optional content, Optional embed = default) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.HasValue ? new[] { embed.Value } : Array.Empty(), this.GetMentions(), default, Array.Empty(), null, default); /// /// Edits the message. @@ -447,8 +447,8 @@ public Task ModifyAsync(Optional content, OptionalThrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(Optional content, Optional> embeds = default) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, Array.Empty(), null, default); + public async Task ModifyAsync(Optional content, Optional> embeds = default) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, Array.Empty(), null, default); /// /// Edits the message. @@ -495,8 +495,8 @@ public async Task ModifyAsync(Action acti /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyEmbedSuppressionAsync(bool hideEmbeds) - => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, Array.Empty(), hideEmbeds ? MessageFlags.SuppressedEmbeds : null, default); + public async Task ModifyEmbedSuppressionAsync(bool hideEmbeds) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, Array.Empty(), hideEmbeds ? MessageFlags.SuppressedEmbeds : null, default); /// /// Deletes the message. @@ -506,8 +506,8 @@ public Task ModifyEmbedSuppressionAsync(bool hideEmbeds) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteAsync(string reason = null) - => this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason); + public async Task DeleteAsync(string reason = null) + => await this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason); /// /// Pins the message in its channel. @@ -517,8 +517,8 @@ public Task DeleteAsync(string reason = null) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task PinAsync() - => this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id); + public async Task PinAsync() + => await this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id); /// /// Unpins the message in its channel. @@ -528,8 +528,8 @@ public Task PinAsync() /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task UnpinAsync() - => this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id); + public async Task UnpinAsync() + => await this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id); /// /// Responds to the message. This produces a reply. @@ -540,8 +540,8 @@ public Task UnpinAsync() /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task RespondAsync(string content) - => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + public async Task RespondAsync(string content) + => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Responds to the message. This produces a reply. @@ -552,8 +552,8 @@ public Task RespondAsync(string content) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task RespondAsync(DiscordEmbed embed) - => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + public async Task RespondAsync(DiscordEmbed embed) + => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Responds to the message. This produces a reply. @@ -565,8 +565,8 @@ public Task RespondAsync(DiscordEmbed embed) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task RespondAsync(string content, DiscordEmbed embed) - => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + public async Task RespondAsync(string content, DiscordEmbed embed) + => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); /// /// Responds to the message. This produces a reply. @@ -577,8 +577,8 @@ public Task RespondAsync(string content, DiscordEmbed embed) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task RespondAsync(DiscordMessageBuilder builder) - => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); + public async Task RespondAsync(DiscordMessageBuilder builder) + => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); /// /// Responds to the message. This produces a reply. @@ -589,11 +589,11 @@ public Task RespondAsync(DiscordMessageBuilder builder) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task RespondAsync(Action action) + public async Task RespondAsync(Action action) { - var builder = new DiscordMessageBuilder(); + DiscordMessageBuilder builder = new DiscordMessageBuilder(); action(builder); - return this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); + return await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); } /// @@ -607,12 +607,11 @@ public Task RespondAsync(Action action) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task CreateThreadAsync(string name, AutoArchiveDuration archiveAfter, string reason = null) + public async Task CreateThreadAsync(string name, AutoArchiveDuration archiveAfter, string reason = null) { - if (this.Channel.Type != ChannelType.Text && this.Channel.Type != ChannelType.News) - throw new InvalidOperationException("Threads can only be created within text or news channels."); - - return this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Channel.Id, this.Id, name, archiveAfter, reason); + return this.Channel.Type != ChannelType.Text && this.Channel.Type != ChannelType.News + ? throw new InvalidOperationException("Threads can only be created within text or news channels.") + : await this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Channel.Id, this.Id, name, archiveAfter, reason); } /// @@ -624,8 +623,8 @@ public Task CreateThreadAsync(string name, AutoArchiveDura /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task CreateReactionAsync(DiscordEmoji emoji) - => this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); + public async Task CreateReactionAsync(DiscordEmoji emoji) + => await this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Deletes your own reaction @@ -635,8 +634,8 @@ public Task CreateReactionAsync(DiscordEmoji emoji) /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteOwnReactionAsync(DiscordEmoji emoji) - => this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); + public async Task DeleteOwnReactionAsync(DiscordEmoji emoji) + => await this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Deletes another user's reaction. @@ -649,8 +648,8 @@ public Task DeleteOwnReactionAsync(DiscordEmoji emoji) /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string reason = null) - => this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); + public async Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string reason = null) + => await this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); /// /// Gets users that reacted with this emoji. @@ -674,8 +673,8 @@ public Task> GetReactionsAsync(DiscordEmoji emoji, in /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteAllReactionsAsync(string reason = null) - => this.Discord.ApiClient.DeleteAllReactionsAsync(this.ChannelId, this.Id, reason); + public async Task DeleteAllReactionsAsync(string reason = null) + => await this.Discord.ApiClient.DeleteAllReactionsAsync(this.ChannelId, this.Id, reason); /// /// Deletes all reactions of a specific reaction for this message. @@ -686,8 +685,8 @@ public Task DeleteAllReactionsAsync(string reason = null) /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteReactionsEmojiAsync(DiscordEmoji emoji) - => this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString()); + public async Task DeleteReactionsEmojiAsync(DiscordEmoji emoji) + => await this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString()); private async Task> GetReactionsInternalAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) { diff --git a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs index 5947e0308c..93c8517a79 100644 --- a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs +++ b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs @@ -36,7 +36,7 @@ public class DiscordOverwrite : SnowflakeObject /// /// Reason as to why this overwrite gets deleted. /// - public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this._channel_id, this.Id, reason); + public async Task DeleteAsync(string reason = null) => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this._channel_id, this.Id, reason); /// /// Updates this channel overwrite. @@ -49,8 +49,8 @@ public class DiscordOverwrite : SnowflakeObject /// Thrown when the overwrite does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task UpdateAsync(Permissions? allow = null, Permissions? deny = null, string reason = null) - => this.Discord.ApiClient.EditChannelPermissionsAsync(this._channel_id, this.Id, allow ?? this.Allowed, deny ?? this.Denied, this.Type.ToString().ToLowerInvariant(), reason); + public async Task UpdateAsync(Permissions? allow = null, Permissions? deny = null, string reason = null) + => await this.Discord.ApiClient.EditChannelPermissionsAsync(this._channel_id, this.Id, allow ?? this.Allowed, deny ?? this.Denied, this.Type.ToString().ToLowerInvariant(), reason); #endregion /// diff --git a/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs b/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs index 41b734d20b..2770e9960e 100644 --- a/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs +++ b/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs @@ -59,15 +59,15 @@ public DiscordChannel Channel /// /// /// Thrown when the client does not have the permission - public Task BecomeSpeakerAsync() - => this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, null); + public async Task BecomeSpeakerAsync() + => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, null); /// /// Request to become a speaker in the stage instance. /// /// /// Thrown when the client does not have the permission - public Task SendSpeakerRequestAsync() => this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, null, DateTime.Now); + public async Task SendSpeakerRequestAsync() => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, null, DateTime.Now); /// /// Invite a member to become a speaker in the state instance. @@ -75,6 +75,6 @@ public Task BecomeSpeakerAsync() /// The member to invite to speak on stage. /// /// Thrown when the client does not have the permission - public Task InviteToSpeakAsync(DiscordMember member) => this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, member.Id, null, suppress: false); + public async Task InviteToSpeakAsync(DiscordMember member) => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, member.Id, null, suppress: false); } } diff --git a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs index 63e8b8ce3a..ba8a1f0247 100644 --- a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs +++ b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs @@ -122,23 +122,37 @@ public async Task RemoveThreadMemberAsync(DiscordMember member) /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { - var mdl = new ThreadChannelEditModel(); + ThreadChannelEditModel mdl = new(); action(mdl); await this.Discord.ApiClient.ModifyThreadChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw, mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), mdl.Bitrate, mdl.Userlimit, mdl.PerUserRateLimit, mdl.RtcRegion.IfPresent(r => r?.Id), - mdl.QualityMode, mdl.Type, mdl.PermissionOverwrites, mdl.IsArchived, mdl.AutoArchiveDuration, mdl.Locked, mdl.AuditLogReason, mdl.AppliedTags); + mdl.QualityMode, mdl.Type, mdl.PermissionOverwrites, mdl.IsArchived, mdl.AutoArchiveDuration, mdl.Locked, mdl.AppliedTags, mdl.AuditLogReason); // We set these *after* the rest request so that Discord can validate the properties. This is useful if the requirements ever change. if (!string.IsNullOrWhiteSpace(mdl.Name)) + { this.Name = mdl.Name; + } + if (mdl.PerUserRateLimit.HasValue) + { this.PerUserRateLimit = mdl.PerUserRateLimit.Value; + } + if (mdl.IsArchived.HasValue) + { this.ThreadMetadata.IsArchived = mdl.IsArchived.Value; + } + if (mdl.AutoArchiveDuration.HasValue) + { this.ThreadMetadata.AutoArchiveDuration = mdl.AutoArchiveDuration.Value; + } + if (mdl.Locked.HasValue) + { this.ThreadMetadata.IsLocked = mdl.Locked.Value; + } } /// diff --git a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs index 4359c9f0ce..c772202e96 100644 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs +++ b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs @@ -56,8 +56,8 @@ public sealed class DiscordForumChannel : DiscordChannel /// /// The builder to create the forum post with. /// The starter (the created thread, and the initial message) from creating the post. - public Task CreateForumPostAsync(ForumPostBuilder builder) - => this.Discord.ApiClient.CreateForumPostAsync(this.Id, builder.Name, builder.AutoArchiveDuration, builder.SlowMode, builder.Message, builder.AppliedTags.Select(t => t.Id)); + public async Task CreateForumPostAsync(ForumPostBuilder builder) + => await this.Discord.ApiClient.CreateForumPostAsync(this.Id, builder.Name, builder.Message, builder.AutoArchiveDuration, builder.SlowMode, builder.AppliedTags.Select(t => t.Id)); internal DiscordForumChannel() {} } diff --git a/DSharpPlus/Entities/Guild/DiscordGuild.cs b/DSharpPlus/Entities/Guild/DiscordGuild.cs index 2c0cd709c5..78cc853892 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuild.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuild.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; @@ -558,7 +557,7 @@ public string GetIconUrl(ImageFormat imageFormat, ushort imageSize = 1024) /// Where this event takes place, up to 100 characters. Only applicable if the type is /// Reason for audit log. /// The created event. - public Task CreateEventAsync(string name, string description, ulong? channelId, ScheduledGuildEventType type, ScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end, string location = null, string reason = null) + public async Task CreateEventAsync(string name, string description, ulong? channelId, ScheduledGuildEventType type, ScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end, string location = null, string reason = null) { if (start <= DateTimeOffset.Now) { @@ -590,7 +589,7 @@ public Task CreateEventAsync(string name, string des }; } - return this.Discord.ApiClient.CreateScheduledGuildEventAsync(this.Id, name, description, channelId, start, end, type, privacyLevel, metadata, reason); + return await this.Discord.ApiClient.CreateScheduledGuildEventAsync(this.Id, name, description, start, type, privacyLevel, metadata, end, channelId, reason); } /// @@ -720,10 +719,10 @@ public async Task ModifyEventAsync(DiscordScheduledGuildEvent guildEvent, Action /// /// /// - public Task DeleteEventAsync(DiscordScheduledGuildEvent guildEvent, string reason = null) + public async Task DeleteEventAsync(DiscordScheduledGuildEvent guildEvent, string reason = null) { this._scheduledEvents.TryRemove(guildEvent.Id, out _); - return this.Discord.ApiClient.DeleteScheduledGuildEventAsync(this.Id, guildEvent.Id); + await this.Discord.ApiClient.DeleteScheduledGuildEventAsync(this.Id, guildEvent.Id); } /// @@ -790,8 +789,8 @@ public async Task> GetEventUsersAsync(DiscordSchedule /// The name to search for. /// The maximum amount of members to return. Max 1000. Defaults to 1. /// The members found, if any. - public Task> SearchMembersAsync(string name, int? limit = 1) - => this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); + public async Task> SearchMembersAsync(string name, int? limit = 1) + => await this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); /// /// Adds a new member to this guild @@ -807,9 +806,9 @@ public Task> SearchMembersAsync(string name, int? l /// Thrown when the or is not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task AddMemberAsync(DiscordUser user, string access_token, string nickname = null, IEnumerable roles = null, + public async Task AddMemberAsync(DiscordUser user, string access_token, string nickname = null, IEnumerable roles = null, bool muted = false, bool deaf = false) - => this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, access_token, nickname, roles, muted, deaf); + => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, access_token, muted, deaf, nickname, roles); /// /// Deletes this guild. Requires the caller to be the owner of the guild. @@ -817,8 +816,8 @@ public Task AddMemberAsync(DiscordUser user, string access_token, string nicknam /// /// Thrown when the client is not the owner of the guild. /// Thrown when Discord is unable to process the request. - public Task DeleteAsync() - => this.Discord.ApiClient.DeleteGuildAsync(this.Id); + public async Task DeleteAsync() + => await this.Discord.ApiClient.DeleteGuildAsync(this.Id); /// /// Modifies this guild. @@ -925,8 +924,8 @@ public async Task> ModifyRolePositionsAsync(IDictiona /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task BanMemberAsync(DiscordMember member, int delete_message_days = 0, string reason = null) - => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, delete_message_days, reason); + public async Task BanMemberAsync(DiscordMember member, int delete_message_days = 0, string reason = null) + => await this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, delete_message_days, reason); /// /// Bans a specified user by ID. This doesn't require the user to be in this guild. @@ -939,8 +938,8 @@ public Task BanMemberAsync(DiscordMember member, int delete_message_days = 0, st /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task BanMemberAsync(ulong user_id, int delete_message_days = 0, string reason = null) - => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, user_id, delete_message_days, reason); + public async Task BanMemberAsync(ulong user_id, int delete_message_days = 0, string reason = null) + => await this.Discord.ApiClient.CreateGuildBanAsync(this.Id, user_id, delete_message_days, reason); /// /// Unbans a user from this guild. @@ -952,8 +951,8 @@ public Task BanMemberAsync(ulong user_id, int delete_message_days = 0, string re /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task UnbanMemberAsync(DiscordUser user, string reason = null) - => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); + public async Task UnbanMemberAsync(DiscordUser user, string reason = null) + => await this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); /// /// Unbans a user by ID. @@ -965,16 +964,16 @@ public Task UnbanMemberAsync(DiscordUser user, string reason = null) /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task UnbanMemberAsync(ulong userId, string reason = null) - => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason); + public async Task UnbanMemberAsync(ulong userId, string reason = null) + => await this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason); /// /// Leaves this guild. /// /// /// Thrown when Discord is unable to process the request. - public Task LeaveAsync() - => this.Discord.ApiClient.LeaveGuildAsync(this.Id); + public async Task LeaveAsync() + => await this.Discord.ApiClient.LeaveGuildAsync(this.Id); /// /// Gets the bans for this guild. @@ -985,8 +984,8 @@ public Task LeaveAsync() /// Collection of bans in this guild. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task> GetBansAsync(int? limit = null, ulong? before = null, ulong? after = null) - => this.Discord.ApiClient.GetGuildBansAsync(this.Id, limit, before, after); + public async Task> GetBansAsync(int? limit = null, ulong? before = null, ulong? after = null) + => await this.Discord.ApiClient.GetGuildBansAsync(this.Id, limit, before, after); /// /// Gets a ban for a specific user. @@ -994,8 +993,8 @@ public Task> GetBansAsync(int? limit = null, ulong? be /// The ID of the user to get the ban for. /// Thrown when the specified user is not banned. /// The requested ban object. - public Task GetBanAsync(ulong userId) - => this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); + public async Task GetBanAsync(ulong userId) + => await this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); /// /// Gets a ban for a specific user. @@ -1083,7 +1082,7 @@ public Task CreateVoiceChannelAsync(string name, DiscordChannel /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task CreateChannelAsync + public async Task CreateChannelAsync ( string name, ChannelType type, @@ -1111,7 +1110,7 @@ public Task CreateChannelAsync return type == ChannelType.Category && parent != null ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) - : this.Discord.ApiClient.CreateGuildChannelAsync + : await this.Discord.ApiClient.CreateGuildChannelAsync ( this.Id, name, @@ -1155,7 +1154,7 @@ public Task DeleteAllChannelsAsync() /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) + public async Task GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) { if (includedRoles != null) { @@ -1172,10 +1171,10 @@ public Task GetPruneCountAsync(int days = 7, IEnumerable inclu } } - return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); + return await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); } - return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); + return await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); } /// @@ -1190,7 +1189,7 @@ public Task GetPruneCountAsync(int days = 7, IEnumerable inclu /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable includedRoles = null, string reason = null) + public async Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable includedRoles = null, string reason = null) { if (includedRoles != null) { @@ -1207,10 +1206,10 @@ public Task GetPruneCountAsync(int days = 7, IEnumerable inclu } } - return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); + return await this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); } - return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); + return await this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); } /// @@ -1221,8 +1220,8 @@ public Task GetPruneCountAsync(int days = 7, IEnumerable inclu /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task> GetIntegrationsAsync() - => this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); + public async Task> GetIntegrationsAsync() + => await this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); /// /// Attaches an integration from current user to this guild. @@ -1233,8 +1232,8 @@ public Task> GetIntegrationsAsync() /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task AttachUserIntegrationAsync(DiscordIntegration integration) - => this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); + public async Task AttachUserIntegrationAsync(DiscordIntegration integration) + => await this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); /// /// Modifies an integration in this guild. @@ -1248,8 +1247,8 @@ public Task AttachUserIntegrationAsync(DiscordIntegration in /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyIntegrationAsync(DiscordIntegration integration, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) - => this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expireBehaviour, expireGracePeriod, enableEmoticons); + public async Task ModifyIntegrationAsync(DiscordIntegration integration, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) + => await this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expireBehaviour, expireGracePeriod, enableEmoticons); /// /// Removes an integration from this guild. @@ -1261,8 +1260,8 @@ public Task ModifyIntegrationAsync(DiscordIntegration integr /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteIntegrationAsync(DiscordIntegration integration, string reason = null) - => this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration, reason); + public async Task DeleteIntegrationAsync(DiscordIntegration integration, string? reason = null) + => await this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration.Id, reason); /// /// Forces re-synchronization of an integration for this guild. @@ -1273,8 +1272,8 @@ public Task DeleteIntegrationAsync(DiscordIntegration integration, string reason /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SyncIntegrationAsync(DiscordIntegration integration) - => this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); + public async Task SyncIntegrationAsync(DiscordIntegration integration) + => await this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); /// /// Gets the voice regions for this guild. @@ -1342,8 +1341,8 @@ public async Task> GetInvitesAsync() /// A partial vanity invite. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task GetVanityInviteAsync() - => this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); + public async Task GetVanityInviteAsync() + => await this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); /// @@ -1352,8 +1351,8 @@ public Task GetVanityInviteAsync() /// A collection of webhooks this guild has. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task> GetWebhooksAsync() - => this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); + public async Task> GetWebhooksAsync() + => await this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); /// /// Gets this guild's widget image. @@ -1485,8 +1484,8 @@ public async Task RequestMembersAsync(string query = "", int limit = 0, bool? pr /// /// A collection of this guild's channels. /// Thrown when Discord is unable to process the request. - public Task> GetChannelsAsync() - => this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); + public async Task> GetChannelsAsync() + => await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); /// /// Creates a new role in this guild. @@ -1502,8 +1501,8 @@ public Task> GetChannelsAsync() /// The newly-created role. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task CreateRoleAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null, Stream icon = null, DiscordEmoji emoji = null) - => this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, reason, icon, emoji?.ToString()); + public async Task CreateRoleAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null, Stream icon = null, DiscordEmoji emoji = null) + => await this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, icon, emoji?.ToString(), reason); /// /// Gets a role from this guild by its ID. /// @@ -1552,7 +1551,7 @@ public async IAsyncEnumerable GetAuditLogsAsync } AuditLog guildAuditLog = await this.Discord.ApiClient.GetAuditLogsAsync(this.Id, remainingEntries, null, - last == 0 ? null : (ulong?)last, byMember?.Id, (int?)actionType); + last == 0 ? null : (ulong?)last, byMember?.Id, actionType); entriesAcquiredLastCall = guildAuditLog.Entries.Count(); totalEntriesCollected += entriesAcquiredLastCall; if (entriesAcquiredLastCall > 0) @@ -1585,8 +1584,8 @@ public async IAsyncEnumerable GetAuditLogsAsync /// /// All of this guild's custom emojis. /// Thrown when Discord is unable to process the request. - public Task> GetEmojisAsync() - => this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); + public async Task> GetEmojisAsync() + => await this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); /// /// Gets this guild's specified custom emoji. @@ -1594,8 +1593,8 @@ public Task> GetEmojisAsync() /// ID of the emoji to get. /// The requested custom emoji. /// Thrown when Discord is unable to process the request. - public Task GetEmojiAsync(ulong id) - => this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); + public async Task GetEmojiAsync(ulong id) + => await this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); /// /// Creates a new custom emoji for this guild. @@ -1607,7 +1606,7 @@ public Task GetEmojiAsync(ulong id) /// The newly-created emoji. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task CreateEmojiAsync(string name, Stream image, IEnumerable roles = null, string reason = null) + public async Task CreateEmojiAsync(string name, Stream image, IEnumerable roles = null, string reason = null) { if (string.IsNullOrWhiteSpace(name)) { @@ -1631,7 +1630,7 @@ public Task CreateEmojiAsync(string name, Stream image, IEnum image64 = imgtool.GetBase64(); } - return this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); + return await this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); } /// @@ -1644,7 +1643,7 @@ public Task CreateEmojiAsync(string name, Stream image, IEnum /// The modified emoji. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable roles = null, string reason = null) + public async Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable roles = null, string reason = null) { if (emoji == null) { @@ -1664,7 +1663,7 @@ public Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name = name.Trim(); return name.Length < 2 || name.Length > 50 ? throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long.") - : this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); + : await this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); } /// @@ -1675,16 +1674,18 @@ public Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string /// /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string reason = null) + public async Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string? reason = null) { - if (emoji == null) + ArgumentNullException.ThrowIfNull(emoji); + + if (emoji.Guild.Id != this.Id) { - throw new ArgumentNullException(nameof(emoji)); + throw new ArgumentException("This emoji does not belong to this guild."); + } + else + { + await this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); } - - return emoji.Guild.Id != this.Id - ? throw new ArgumentException("This emoji does not belong to this guild.") - : this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); } /// @@ -1702,15 +1703,15 @@ public DiscordChannel GetDefaultChannel() => /// Gets the guild's widget /// /// The guild's widget - public Task GetWidgetAsync() - => this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); + public async Task GetWidgetAsync() + => await this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); /// /// Gets the guild's widget settings /// /// The guild's widget settings - public Task GetWidgetSettingsAsync() - => this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); + public async Task GetWidgetSettingsAsync() + => await this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); /// /// Modifies the guild's widget settings @@ -1719,8 +1720,8 @@ public Task GetWidgetSettingsAsync() /// Widget channel /// Reason the widget settings were modified /// The newly modified widget settings - public Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel channel = null, string reason = null) - => this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); + public async Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel channel = null, string reason = null) + => await this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); /// /// Gets all of this guild's templates. @@ -1728,8 +1729,8 @@ public Task ModifyWidgetSettingsAsync(bool? isEnabled = n /// All of the guild's templates. /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task> GetTemplatesAsync() - => this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); + public async Task> GetTemplatesAsync() + => await this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); /// /// Creates a guild template. @@ -1740,8 +1741,8 @@ public Task> GetTemplatesAsync() /// Throws when a template already exists for the guild or a null parameter is provided for the name. /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task CreateTemplateAsync(string name, string description = null) - => this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); + public async Task CreateTemplateAsync(string name, string description = null) + => await this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); /// /// Syncs the template to the current guild's state. @@ -1751,8 +1752,8 @@ public Task CreateTemplateAsync(string name, string descri /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task SyncTemplateAsync(string code) - => this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); + public async Task SyncTemplateAsync(string code) + => await this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); /// /// Modifies the template's metadata. @@ -1764,8 +1765,8 @@ public Task SyncTemplateAsync(string code) /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task ModifyTemplateAsync(string code, string name = null, string description = null) - => this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); + public async Task ModifyTemplateAsync(string code, string name = null, string description = null) + => await this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); /// /// Deletes the template. @@ -1775,16 +1776,16 @@ public Task ModifyTemplateAsync(string code, string name = /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task DeleteTemplateAsync(string code) - => this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); + public async Task DeleteTemplateAsync(string code) + => await this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); /// /// Gets this guild's membership screening form. /// /// This guild's membership screening form. /// Thrown when Discord is unable to process the request. - public Task GetMembershipScreeningFormAsync() - => this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); + public async Task GetMembershipScreeningFormAsync() + => await this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); /// /// Modifies this guild's membership screening form. @@ -1793,27 +1794,27 @@ public Task GetMembershipScreeningFormAsync() /// The modified screening form. /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. /// Thrown when Discord is unable to process the request. - public Task ModifyMembershipScreeningFormAsync(Action action) + public async Task ModifyMembershipScreeningFormAsync(Action action) { MembershipScreeningEditModel editModel = new(); action(editModel); - return this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, editModel.Enabled, editModel.Fields, editModel.Description); + return await this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, editModel.Enabled, editModel.Fields, editModel.Description); } /// /// Gets a list of stickers from this guild. /// /// - public Task> GetStickersAsync() - => this.Discord.ApiClient.GetGuildStickersAsync(this.Id); + public async Task> GetStickersAsync() + => await this.Discord.ApiClient.GetGuildStickersAsync(this.Id); /// /// Gets a sticker from this guild. /// /// The id of the sticker. /// - public Task GetStickerAsync(ulong stickerId) - => this.Discord.ApiClient.GetGuildStickerAsync(this.Id, stickerId); + public async Task GetStickerAsync(ulong stickerId) + => await this.Discord.ApiClient.GetGuildStickerAsync(this.Id, stickerId); /// /// Creates a sticker in this guild. Lottie stickers can only be created on verified and/or partnered servers. @@ -1825,7 +1826,7 @@ public Task GetStickerAsync(ulong stickerId) /// The image format of the sticker. /// The reason this sticker is being created. - public Task CreateStickerAsync(string name, string description, string tags, Stream imageContents, StickerFormat format, string reason = null) + public async Task CreateStickerAsync(string name, string description, string tags, Stream imageContents, StickerFormat format, string reason = null) { string contentType = null, extension = null; @@ -1845,7 +1846,7 @@ public Task CreateStickerAsync(string name, string descri extension = "json"; } - return this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description ?? string.Empty, tags, new DiscordMessageFile(null, imageContents, null, extension, contentType), reason); + return await this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description ?? string.Empty, tags, new DiscordMessageFile(null, imageContents, null, extension, contentType), reason); } /// @@ -1854,11 +1855,11 @@ public Task CreateStickerAsync(string name, string descri /// The id of the sticker. /// Action to perform. /// Reason for audit log. - public Task ModifyStickerAsync(ulong stickerId, Action action, string reason = null) + public async Task ModifyStickerAsync(ulong stickerId, Action action, string reason = null) { StickerEditModel editModel = new(); action(editModel); - return this.Discord.ApiClient.ModifyStickerAsync(this.Id, stickerId, editModel.Name, editModel.Description, editModel.Tags, reason ?? editModel.AuditLogReason); + return await this.Discord.ApiClient.ModifyStickerAsync(this.Id, stickerId, editModel.Name, editModel.Description, editModel.Tags, reason ?? editModel.AuditLogReason); } /// @@ -1867,11 +1868,11 @@ public Task ModifyStickerAsync(ulong stickerId, ActionSticker to modify. /// Action to perform. /// Reason for audit log. - public Task ModifyStickerAsync(DiscordMessageSticker sticker, Action action, string reason = null) + public async Task ModifyStickerAsync(DiscordMessageSticker sticker, Action action, string reason = null) { StickerEditModel editModel = new(); action(editModel); - return this.Discord.ApiClient.ModifyStickerAsync(this.Id, sticker.Id, editModel.Name, editModel.Description, editModel.Tags, reason ?? editModel.AuditLogReason); + return await this.Discord.ApiClient.ModifyStickerAsync(this.Id, sticker.Id, editModel.Name, editModel.Description, editModel.Tags, reason ?? editModel.AuditLogReason); } /// @@ -1879,39 +1880,39 @@ public Task ModifyStickerAsync(DiscordMessageSticker stic /// /// The id of the sticker. /// Reason for audit log. - public Task DeleteStickerAsync(ulong stickerId, string reason = null) - => this.Discord.ApiClient.DeleteStickerAsync(this.Id, stickerId, reason); + public async Task DeleteStickerAsync(ulong stickerId, string reason = null) + => await this.Discord.ApiClient.DeleteStickerAsync(this.Id, stickerId, reason); /// /// Deletes a sticker in this guild. /// /// Sticker to delete. /// Reason for audit log. - public Task DeleteStickerAsync(DiscordMessageSticker sticker, string reason = null) - => this.Discord.ApiClient.DeleteStickerAsync(this.Id, sticker.Id, reason); + public async Task DeleteStickerAsync(DiscordMessageSticker sticker, string reason = null) + => await this.Discord.ApiClient.DeleteStickerAsync(this.Id, sticker.Id, reason); /// /// Gets all the application commands in this guild. /// /// A list of application commands in this guild. - public Task> GetApplicationCommandsAsync() => - this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id); + public async Task> GetApplicationCommandsAsync() => + await this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id); /// /// Overwrites the existing application commands in this guild. New commands are automatically created and missing commands are automatically delete /// /// The list of commands to overwrite with. /// The list of guild commands - public Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => - this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); + public async Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => + await this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); /// /// Creates or overwrites a application command in this guild. /// /// The command to create. /// The created command. - public Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => - this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); + public async Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => + await this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); /// /// Edits a application command in this guild. @@ -1931,8 +1932,8 @@ public async Task EditApplicationCommandAsync(ulong c /// /// The ID of the command to get. /// The command with the ID. - public Task GetApplicationCommandAsync(ulong commandId) => - this.Discord.ApiClient.GetGlobalApplicationCommandAsync(this.Discord.CurrentApplication.Id, commandId); + public async Task GetApplicationCommandAsync(ulong commandId) => + await this.Discord.ApiClient.GetGlobalApplicationCommandAsync(this.Discord.CurrentApplication.Id, commandId); /// /// Gets a application command in this guild by its name. @@ -1957,8 +1958,8 @@ public async Task GetApplicationCommandAsync(string c /// /// This guild's welcome screen object. /// Thrown when Discord is unable to process the request. - public Task GetWelcomeScreenAsync() => - this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); + public async Task GetWelcomeScreenAsync() => + await this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); /// /// Modifies this guild's welcome screen. @@ -1979,16 +1980,16 @@ public async Task ModifyWelcomeScreenAsync(Action /// A list of permissions. - public Task> GetApplicationCommandsPermissionsAsync() - => this.Discord.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id); + public async Task> GetApplicationCommandsPermissionsAsync() + => await this.Discord.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id); /// /// Gets permissions for a application command in this guild. /// /// The command to get them for. /// The permissions. - public Task GetApplicationCommandPermissionsAsync(DiscordApplicationCommand command) - => this.Discord.ApiClient.GetApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, command.Id); + public async Task GetApplicationCommandPermissionsAsync(DiscordApplicationCommand command) + => await this.Discord.ApiClient.GetApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, command.Id); /// /// Edits permissions for a application command in this guild. @@ -1996,16 +1997,16 @@ public Task GetApplicationCommandPerm /// The command to edit permissions for. /// The list of permissions to use. /// The edited permissions. - public Task EditApplicationCommandPermissionsAsync(DiscordApplicationCommand command, IEnumerable permissions) - => this.Discord.ApiClient.EditApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, command.Id, permissions); + public async Task EditApplicationCommandPermissionsAsync(DiscordApplicationCommand command, IEnumerable permissions) + => await this.Discord.ApiClient.EditApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, command.Id, permissions); /// /// Batch edits permissions for a application command in this guild. /// /// The list of permissions to use. /// A list of edited permissions. - public Task> BatchEditApplicationCommandPermissionsAsync(IEnumerable permissions) - => this.Discord.ApiClient.BatchEditApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, permissions); + public async Task> BatchEditApplicationCommandPermissionsAsync(IEnumerable permissions) + => await this.Discord.ApiClient.BatchEditApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, permissions); /// /// Creates an auto-moderation rule in the guild. @@ -2020,7 +2021,7 @@ public Task> BatchEditA /// Channels which will not trigger the rule. /// Reason for audit logs. /// The created rule. - public Task CreateAutoModerationRuleAsync + public async Task CreateAutoModerationRuleAsync ( string name, RuleEventType eventType, @@ -2032,7 +2033,7 @@ public Task CreateAutoModerationRuleAsync Optional> exemptChannels = default, string reason = null ) => - this.Discord.ApiClient.CreateGuildAutoModerationRuleAsync + await this.Discord.ApiClient.CreateGuildAutoModerationRuleAsync ( this.Id, name, @@ -2051,15 +2052,15 @@ public Task CreateAutoModerationRuleAsync /// /// The rule id. /// The found rule. - public Task GetAutoModerationRuleAsync(ulong ruleId) - => this.Discord.ApiClient.GetGuildAutoModerationRuleAsync(this.Id, ruleId); + public async Task GetAutoModerationRuleAsync(ulong ruleId) + => await this.Discord.ApiClient.GetGuildAutoModerationRuleAsync(this.Id, ruleId); /// /// Gets all auto-moderation rules in the guild. /// /// All rules available in the guild. - public Task> GetAutoModerationRulesAsync() - => this.Discord.ApiClient.GetGuildAutoModerationRulesAsync(this.Id); + public async Task> GetAutoModerationRulesAsync() + => await this.Discord.ApiClient.GetGuildAutoModerationRulesAsync(this.Id); /// /// Modify an auto-moderation rule in the guild. @@ -2068,13 +2069,13 @@ public Task> GetAutoModerationRulesAsyn /// Action to perform on this rule. /// The modified rule. /// All arguments are optionals. - public Task ModifyAutoModerationRuleAsync(ulong ruleId, Action action) + public async Task ModifyAutoModerationRuleAsync(ulong ruleId, Action action) { AutoModerationRuleEditModel model = new(); action(model); - return this.Discord.ApiClient.ModifyGuildAutoModerationRuleAsync + return await this.Discord.ApiClient.ModifyGuildAutoModerationRuleAsync ( this.Id, ruleId, @@ -2095,8 +2096,8 @@ public Task ModifyAutoModerationRuleAsync(ulong ruleI /// The rule id. /// Reason for audit logs. /// - public Task DeleteAutoModerationRuleAsync(ulong ruleId, string reason = null) - => this.Discord.ApiClient.DeleteGuildAutoModerationRuleAsync(this.Id, ruleId, reason); + public async Task DeleteAutoModerationRuleAsync(ulong ruleId, string reason = null) + => await this.Discord.ApiClient.DeleteGuildAutoModerationRuleAsync(this.Id, ruleId, reason); #endregion diff --git a/DSharpPlus/Entities/Guild/DiscordMember.cs b/DSharpPlus/Entities/Guild/DiscordMember.cs index 88c8662b71..76b3040c06 100644 --- a/DSharpPlus/Entities/Guild/DiscordMember.cs +++ b/DSharpPlus/Entities/Guild/DiscordMember.cs @@ -322,14 +322,16 @@ public override string? GlobalName /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task CreateDmChannelAsync() + public async Task CreateDmChannelAsync() { DiscordDmChannel dm = default; if (this.Discord is DiscordClient dc) + { dm = dc._privateChannels.Values.FirstOrDefault(x => x.Recipients.FirstOrDefault() == this); + } - return dm != null ? Task.FromResult(dm) : this.Discord.ApiClient.CreateDmAsync(this.Id); + return dm is not null ? dm : await this.Discord.ApiClient.CreateDmAsync(this.Id); } /// @@ -410,8 +412,8 @@ public async Task SendMessageAsync(DiscordMessageBuilder message /// /// How long the timeout should last. Set to or a time in the past to remove the timeout. /// Why this member is being restricted. - public Task TimeoutAsync(DateTimeOffset? until, string reason = default) - => this.Discord.ApiClient.ModifyGuildMemberAsync(this._guild_id, this.Id, default, default, default, default, default, until, reason); + public async Task TimeoutAsync(DateTimeOffset? until, string reason = default) + => await this.Discord.ApiClient.ModifyGuildMemberAsync(this._guild_id, this.Id, default, default, default, default, default, until, reason); /// /// Sets this member's voice mute status. @@ -423,8 +425,8 @@ public Task TimeoutAsync(DateTimeOffset? until, string reason = default) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SetMuteAsync(bool mute, string reason = null) - => this.Discord.ApiClient.ModifyGuildMemberAsync(this._guild_id, this.Id, default, default, mute, default, default, default, reason); + public async Task SetMuteAsync(bool mute, string reason = null) + => await this.Discord.ApiClient.ModifyGuildMemberAsync(this._guild_id, this.Id, default, default, mute, default, default, default, reason); /// /// Sets this member's voice deaf status. @@ -436,8 +438,8 @@ public Task SetMuteAsync(bool mute, string reason = null) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task SetDeafAsync(bool deaf, string reason = null) - => this.Discord.ApiClient.ModifyGuildMemberAsync(this._guild_id, this.Id, default, default, default, deaf, default, default, reason); + public async Task SetDeafAsync(bool deaf, string reason = null) + => await this.Discord.ApiClient.ModifyGuildMemberAsync(this._guild_id, this.Id, default, default, default, deaf, default, default, reason); /// /// Modifies this member. @@ -483,8 +485,8 @@ await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, mdl. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task GrantRoleAsync(DiscordRole role, string reason = null) - => this.Discord.ApiClient.AddGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); + public async Task GrantRoleAsync(DiscordRole role, string reason = null) + => await this.Discord.ApiClient.AddGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); /// /// Revokes a role from a member. @@ -496,8 +498,8 @@ public Task GrantRoleAsync(DiscordRole role, string reason = null) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task RevokeRoleAsync(DiscordRole role, string reason = null) - => this.Discord.ApiClient.RemoveGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); + public async Task RevokeRoleAsync(DiscordRole role, string reason = null) + => await this.Discord.ApiClient.RemoveGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); /// /// Sets the member's roles to ones specified. @@ -509,8 +511,8 @@ public Task RevokeRoleAsync(DiscordRole role, string reason = null) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ReplaceRolesAsync(IEnumerable roles, string reason = null) - => this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, default, + public async Task ReplaceRolesAsync(IEnumerable roles, string reason = null) + => await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, default, new Optional>(roles.Select(xr => xr.Id)), default, default, default, default, reason); /// @@ -542,8 +544,8 @@ public Task BanAsync(int delete_message_days = 0, string reason = null) /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task RemoveAsync(string reason = null) - => this.Discord.ApiClient.RemoveGuildMemberAsync(this._guild_id, this.Id, reason); + public async Task RemoveAsync(string reason = null) + => await this.Discord.ApiClient.RemoveGuildMemberAsync(this._guild_id, this.Id, reason); /// /// Moves this member to the specified voice channel diff --git a/DSharpPlus/Entities/Guild/DiscordRole.cs b/DSharpPlus/Entities/Guild/DiscordRole.cs index e677707c68..32b2724ae7 100644 --- a/DSharpPlus/Entities/Guild/DiscordRole.cs +++ b/DSharpPlus/Entities/Guild/DiscordRole.cs @@ -104,18 +104,20 @@ public string Mention /// Thrown when the role does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyPositionAsync(int position, string reason = null) + public async Task ModifyPositionAsync(int position, string reason = null) { - var roles = this.Discord.Guilds[this._guild_id].Roles.Values.OrderByDescending(xr => xr.Position).ToArray(); - var pmds = new RestGuildRoleReorderPayload[roles.Length]; - for (var i = 0; i < roles.Length; i++) + DiscordRole[] roles = this.Discord.Guilds[this._guild_id].Roles.Values.OrderByDescending(xr => xr.Position).ToArray(); + RestGuildRoleReorderPayload[] pmds = new RestGuildRoleReorderPayload[roles.Length]; + for (int i = 0; i < roles.Length; i++) { - pmds[i] = new RestGuildRoleReorderPayload { RoleId = roles[i].Id }; - - pmds[i].Position = roles[i].Id == this.Id ? position : roles[i].Position <= position ? roles[i].Position - 1 : roles[i].Position; + pmds[i] = new RestGuildRoleReorderPayload + { + RoleId = roles[i].Id, + Position = roles[i].Id == this.Id ? position : roles[i].Position <= position ? roles[i].Position - 1 : roles[i].Position + }; } - return this.Discord.ApiClient.ModifyGuildRolePositionAsync(this._guild_id, pmds, reason); + await this.Discord.ApiClient.ModifyGuildRolePositionsAsync(this._guild_id, pmds, reason); } /// @@ -134,8 +136,8 @@ public Task ModifyPositionAsync(int position, string reason = null) /// Thrown when the role does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null, Stream icon = null, DiscordEmoji emoji = null) - => this.Discord.ApiClient.ModifyGuildRoleAsync(this._guild_id, this.Id, name, permissions, color?.Value, hoist, mentionable, reason, icon, emoji?.ToString()); + public async Task ModifyAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null, Stream icon = null, DiscordEmoji emoji = null) + => await this.Discord.ApiClient.ModifyGuildRoleAsync(this._guild_id, this.Id, name, permissions, color?.Value, hoist, mentionable, icon, emoji?.ToString(), reason); /// Thrown when the client does not have the permission. /// Thrown when the role does not exist. @@ -158,7 +160,8 @@ public Task ModifyAsync(Action action) /// Thrown when the role does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteRoleAsync(this._guild_id, this.Id, reason); + public async Task DeleteAsync(string? reason = null) + => await this.Discord.ApiClient.DeleteRoleAsync(this._guild_id, this.Id, reason); #endregion internal DiscordRole() { } diff --git a/DSharpPlus/Entities/Interaction/DiscordInteraction.cs b/DSharpPlus/Entities/Interaction/DiscordInteraction.cs index 8337997297..ac1940e4b5 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteraction.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteraction.cs @@ -96,8 +96,8 @@ public DiscordChannel Channel /// /// The type of the response. /// The data, if any, to send. - public Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null) => - this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, builder); + public async Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null) => + await this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, builder); /// /// Creates a deferred response to this interaction. @@ -111,8 +111,8 @@ public Task DeferAsync(bool ephemeral = false) => this.CreateResponseAsync( /// Gets the original interaction response. /// /// The original message that was sent. - public Task GetOriginalResponseAsync() => - this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); + public async Task GetOriginalResponseAsync() => + await this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); /// /// Edits the original interaction response. @@ -130,8 +130,8 @@ public async Task EditOriginalResponseAsync(DiscordWebhookBuilde /// /// Deletes the original interaction response. /// > - public Task DeleteOriginalResponseAsync() => - this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); + public async Task DeleteOriginalResponseAsync() => + await this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); /// /// Creates a follow up message to this interaction. @@ -149,8 +149,8 @@ public async Task CreateFollowupMessageAsync(DiscordFollowupMess /// Gets a follow up message. /// /// The id of the follow up message. - public Task GetFollowupMessageAsync(ulong messageId) => - this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); + public async Task GetFollowupMessageAsync(ulong messageId) => + await this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); /// /// Edits a follow up message. @@ -170,7 +170,7 @@ public async Task EditFollowupMessageAsync(ulong messageId, Disc /// Deletes a follow up message. /// /// The id of the follow up message. - public Task DeleteFollowupMessageAsync(ulong messageId) => - this.Discord.ApiClient.DeleteFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); + public async Task DeleteFollowupMessageAsync(ulong messageId) => + await this.Discord.ApiClient.DeleteFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); } } diff --git a/DSharpPlus/Entities/Invite/DiscordInvite.cs b/DSharpPlus/Entities/Invite/DiscordInvite.cs index d1ffd0b54c..6221f2d064 100644 --- a/DSharpPlus/Entities/Invite/DiscordInvite.cs +++ b/DSharpPlus/Entities/Invite/DiscordInvite.cs @@ -128,8 +128,8 @@ internal DiscordInvite() { } /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteAsync(string reason = null) - => this.Discord.ApiClient.DeleteInviteAsync(this.Code, reason); + public async Task DeleteAsync(string reason = null) + => await this.Discord.ApiClient.DeleteInviteAsync(this.Code, reason); /* * Disabled due to API restrictions. diff --git a/DSharpPlus/Entities/Webhook/DiscordWebhook.cs b/DSharpPlus/Entities/Webhook/DiscordWebhook.cs index f22a4e8baf..585ca64407 100644 --- a/DSharpPlus/Entities/Webhook/DiscordWebhook.cs +++ b/DSharpPlus/Entities/Webhook/DiscordWebhook.cs @@ -88,18 +88,22 @@ internal DiscordWebhook() { } /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(string name = null, Optional avatar = default, ulong? channelId = null, string reason = null) + public async Task ModifyAsync(string name = null, Optional avatar = default, ulong? channelId = null, string reason = null) { - var avatarb64 = Optional.FromNoValue(); + Optional avatarb64 = Optional.FromNoValue(); if (avatar.HasValue && avatar.Value != null) - using (var imgtool = new ImageTool(avatar.Value)) - avatarb64 = imgtool.GetBase64(); + { + using ImageTool imgtool = new(avatar.Value); + avatarb64 = imgtool.GetBase64(); + } else if (avatar.HasValue) + { avatarb64 = null; + } - var newChannelId = channelId ?? this.ChannelId; + ulong newChannelId = channelId ?? this.ChannelId; - return this.Discord.ApiClient.ModifyWebhookAsync(this.Id, newChannelId, name, avatarb64, reason); + return await this.Discord.ApiClient.ModifyWebhookAsync(this.Id, newChannelId, name, avatarb64, reason); } /// @@ -110,8 +114,8 @@ public Task ModifyAsync(string name = null, Optional ava /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteAsync() - => this.Discord.ApiClient.DeleteWebhookAsync(this.Id, this.Token); + public async Task DeleteAsync() + => await this.Discord.ApiClient.DeleteWebhookAsync(this.Id, this.Token); /// /// Executes this webhook with the given . @@ -120,8 +124,8 @@ public Task DeleteAsync() /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ExecuteAsync(DiscordWebhookBuilder builder) - => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookAsync(this.Id, this.Token, builder); + public async Task ExecuteAsync(DiscordWebhookBuilder builder) + => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookAsync(this.Id, this.Token, builder); /// /// Executes this webhook in Slack compatibility mode. @@ -131,8 +135,8 @@ public Task ExecuteAsync(DiscordWebhookBuilder builder) /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ExecuteSlackAsync(string json) - => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookSlackAsync(this.Id, this.Token, json); + public async Task ExecuteSlackAsync(string json) + => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookSlackAsync(this.Id, this.Token, json); /// /// Executes this webhook in GitHub compatibility mode. @@ -142,8 +146,8 @@ public Task ExecuteSlackAsync(string json) /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task ExecuteGithubAsync(string json) - => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookGithubAsync(this.Id, this.Token, json); + public async Task ExecuteGithubAsync(string json) + => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookGithubAsync(this.Id, this.Token, json); /// /// Gets a previously-sent webhook message. @@ -179,8 +183,8 @@ public async Task EditMessageAsync(ulong messageId, DiscordWebho /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task DeleteMessageAsync(ulong messageId) - => (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId); + public async Task DeleteMessageAsync(ulong messageId) + => await (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId); /// /// Checks whether this is equal to another object. diff --git a/DSharpPlus/Exceptions/BadRequestException.cs b/DSharpPlus/Exceptions/BadRequestException.cs index 4bf2786a09..994db8fe8f 100644 --- a/DSharpPlus/Exceptions/BadRequestException.cs +++ b/DSharpPlus/Exceptions/BadRequestException.cs @@ -1,6 +1,5 @@ -using System; -using DSharpPlus.Net; -using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Text.Json; namespace DSharpPlus.Exceptions; @@ -20,25 +19,44 @@ public class BadRequestException : DiscordException /// public string? Errors { get; internal set; } - internal BadRequestException(BaseRestRequest request, RestResponse response) : base("Bad request: " + response.ResponseCode) + internal BadRequestException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Bad request: " + response.StatusCode) { - this.WebRequest = request; - this.WebResponse = response; + this.Request = request; + this.Response = response; - JObject jsonResponse = JObject.Parse(response.Response); - if (jsonResponse.TryGetValue("message", StringComparison.Ordinal, out JToken? message)) + try { - this.JsonMessage = message.ToString(); - } - - if (jsonResponse.TryGetValue("code", StringComparison.Ordinal, out JToken? code)) - { - this.Code = (int)code; - } - - if (jsonResponse.TryGetValue("errors", StringComparison.Ordinal, out JToken? errors)) - { - this.Errors = errors.ToString(); + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("code", out JsonElement code) + && code.ValueKind == JsonValueKind.Number + ) + { + this.Code = code.GetInt32(); + } + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } + + if + ( + responseModel.TryGetProperty("errors", out JsonElement errors) + && message.ValueKind == JsonValueKind.String + ) + { + this.Errors = errors.GetString(); + } } + catch { } } } diff --git a/DSharpPlus/Exceptions/DiscordException.cs b/DSharpPlus/Exceptions/DiscordException.cs index e862d78b27..c2634a4ae8 100644 --- a/DSharpPlus/Exceptions/DiscordException.cs +++ b/DSharpPlus/Exceptions/DiscordException.cs @@ -1,5 +1,5 @@ using System; -using DSharpPlus.Net; +using System.Net.Http; namespace DSharpPlus.Exceptions; @@ -8,21 +8,18 @@ public abstract class DiscordException : Exception /// /// Gets the request that caused the exception. /// - public virtual BaseRestRequest WebRequest { get; internal set; } = null!; + public virtual HttpRequestMessage? Request { get; internal set; } /// /// Gets the response to the request. /// - public virtual RestResponse WebResponse { get; internal set; } = null!; + public virtual HttpResponseMessage? Response { get; internal set; } /// /// Gets the JSON message received. /// public virtual string? JsonMessage { get; internal set; } - - /// - public override string Message => $"{base.Message}. Json Message: {JsonMessage ?? "Discord did not provide an error message."}"; - + public DiscordException() : base() { } public DiscordException(string message) : base(message) { } public DiscordException(string message, Exception innerException) : base(message, innerException) { } diff --git a/DSharpPlus/Exceptions/NotFoundException.cs b/DSharpPlus/Exceptions/NotFoundException.cs index 3493c8079d..d6c4bd1557 100644 --- a/DSharpPlus/Exceptions/NotFoundException.cs +++ b/DSharpPlus/Exceptions/NotFoundException.cs @@ -1,6 +1,5 @@ -using System; -using DSharpPlus.Net; -using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Text.Json; namespace DSharpPlus.Exceptions; @@ -9,13 +8,26 @@ namespace DSharpPlus.Exceptions; /// public class NotFoundException : DiscordException { - internal NotFoundException(BaseRestRequest request, RestResponse response) : base("Not found: " + response.ResponseCode) + internal NotFoundException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Not found: " + response.StatusCode) { - this.WebRequest = request; - this.WebResponse = response; - if (JObject.Parse(response.Response).TryGetValue("message", StringComparison.Ordinal, out JToken? message)) + this.Request = request; + this.Response = response; + + try { - this.JsonMessage = message.ToString(); + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } } + catch { } } } diff --git a/DSharpPlus/Exceptions/RateLimitException.cs b/DSharpPlus/Exceptions/RateLimitException.cs index 9d4ba41c82..7dbeeb415c 100644 --- a/DSharpPlus/Exceptions/RateLimitException.cs +++ b/DSharpPlus/Exceptions/RateLimitException.cs @@ -1,6 +1,5 @@ -using System; -using DSharpPlus.Net; -using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Text.Json; namespace DSharpPlus.Exceptions; @@ -9,13 +8,26 @@ namespace DSharpPlus.Exceptions; /// public class RateLimitException : DiscordException { - internal RateLimitException(BaseRestRequest request, RestResponse response) : base("Rate limited: " + response.ResponseCode) + internal RateLimitException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Rate limited: " + response.StatusCode) { - this.WebRequest = request; - this.WebResponse = response; - if (JObject.Parse(response.Response).TryGetValue("message", StringComparison.Ordinal, out JToken? message)) + this.Request = request; + this.Response = response; + + try { - this.JsonMessage = message.ToString(); + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } } + catch { } } } diff --git a/DSharpPlus/Exceptions/RequestSizeException.cs b/DSharpPlus/Exceptions/RequestSizeException.cs index 885b502d8a..23e7114674 100644 --- a/DSharpPlus/Exceptions/RequestSizeException.cs +++ b/DSharpPlus/Exceptions/RequestSizeException.cs @@ -1,6 +1,5 @@ -using System; -using DSharpPlus.Net; -using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Text.Json; namespace DSharpPlus.Exceptions; @@ -9,13 +8,26 @@ namespace DSharpPlus.Exceptions; /// public class RequestSizeException : DiscordException { - internal RequestSizeException(BaseRestRequest request, RestResponse response) : base($"Request entity too large: {response.ResponseCode}. Make sure the data sent is within Discord's upload limit") + internal RequestSizeException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base($"Request entity too large: {response.StatusCode}. Make sure the data sent is within Discord's upload limit.") { - this.WebRequest = request; - this.WebResponse = response; - if (JObject.Parse(response.Response).TryGetValue("message", StringComparison.Ordinal, out JToken? message)) + this.Request = request; + this.Response = response; + + try { - this.JsonMessage = message.ToString(); + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } } + catch { } } } diff --git a/DSharpPlus/Exceptions/ServerErrorException.cs b/DSharpPlus/Exceptions/ServerErrorException.cs index 7422cee39d..1ad33dbf00 100644 --- a/DSharpPlus/Exceptions/ServerErrorException.cs +++ b/DSharpPlus/Exceptions/ServerErrorException.cs @@ -1,6 +1,5 @@ -using System; -using DSharpPlus.Net; -using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Text.Json; namespace DSharpPlus.Exceptions; @@ -9,13 +8,26 @@ namespace DSharpPlus.Exceptions; /// public class ServerErrorException : DiscordException { - internal ServerErrorException(BaseRestRequest request, RestResponse response) : base("Internal Server Error: " + response.ResponseCode) + internal ServerErrorException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Internal Server Error: " + response.StatusCode) { - this.WebRequest = request; - this.WebResponse = response; - if (JObject.Parse(response.Response).TryGetValue("message", StringComparison.Ordinal, out JToken? message)) + this.Request = request; + this.Response = response; + + try { - this.JsonMessage = message.ToString(); + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } } + catch { } } } diff --git a/DSharpPlus/Exceptions/UnauthorizedException.cs b/DSharpPlus/Exceptions/UnauthorizedException.cs index 8d90818bcd..b94cc993e1 100644 --- a/DSharpPlus/Exceptions/UnauthorizedException.cs +++ b/DSharpPlus/Exceptions/UnauthorizedException.cs @@ -1,6 +1,5 @@ -using System; -using DSharpPlus.Net; -using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Text.Json; namespace DSharpPlus.Exceptions; @@ -9,13 +8,26 @@ namespace DSharpPlus.Exceptions; /// public class UnauthorizedException : DiscordException { - internal UnauthorizedException(BaseRestRequest request, RestResponse response) : base("Unauthorized: " + response.ResponseCode) + internal UnauthorizedException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Unauthorized: " + response.StatusCode) { - this.WebRequest = request; - this.WebResponse = response; - if (JObject.Parse(response.Response).TryGetValue("message", StringComparison.Ordinal, out JToken? message)) + this.Request = request; + this.Response = response; + + try { - this.JsonMessage = message.ToString(); + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } } + catch { } } } diff --git a/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index aba023d380..24862c4e13 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -2,104 +2,103 @@ using DSharpPlus.Entities; using Newtonsoft.Json; -namespace DSharpPlus.Net.Abstractions +namespace DSharpPlus.Net.Abstractions; + +internal class RestApplicationCommandCreatePayload { - internal class RestApplicationCommandCreatePayload - { - [JsonProperty("type")] - public ApplicationCommandType Type { get; set; } + [JsonProperty("type")] + public ApplicationCommandType Type { get; set; } - [JsonProperty("name")] - public string Name { get; set; } + [JsonProperty("name")] + public string Name { get; set; } - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } - [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Options { get; set; } + [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Options { get; set; } - [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] - public bool? DefaultPermission { get; set; } + [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] + public bool? DefaultPermission { get; set; } - [JsonProperty("name_localizations")] - public IReadOnlyDictionary NameLocalizations { get; set; } + [JsonProperty("name_localizations")] + public IReadOnlyDictionary NameLocalizations { get; set; } - [JsonProperty("description_localizations")] - public IReadOnlyDictionary DescriptionLocalizations{ get; set; } + [JsonProperty("description_localizations")] + public IReadOnlyDictionary DescriptionLocalizations{ get; set; } - [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] - public bool? AllowDMUsage { get; set; } + [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] + public bool? AllowDMUsage { get; set; } - [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] - public Permissions? DefaultMemberPermissions { get; set; } + [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] + public Permissions? DefaultMemberPermissions { get; set; } - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? NSFW { get; set; } - } + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? NSFW { get; set; } +} - internal class RestApplicationCommandEditPayload - { - [JsonProperty("name")] - public Optional Name { get; set; } +internal class RestApplicationCommandEditPayload +{ + [JsonProperty("name")] + public Optional Name { get; set; } - [JsonProperty("description")] - public Optional Description { get; set; } + [JsonProperty("description")] + public Optional Description { get; set; } - [JsonProperty("options")] - public Optional> Options { get; set; } + [JsonProperty("options")] + public Optional> Options { get; set; } - [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultPermission { get; set; } + [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultPermission { get; set; } - [JsonProperty("name_localizations")] - public IReadOnlyDictionary NameLocalizations { get; set; } + [JsonProperty("name_localizations")] + public IReadOnlyDictionary? NameLocalizations { get; set; } - [JsonProperty("description_localizations")] - public IReadOnlyDictionary DescriptionLocalizations{ get; set; } + [JsonProperty("description_localizations")] + public IReadOnlyDictionary? DescriptionLocalizations{ get; set; } - [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] - public Optional AllowDMUsage { get; set; } + [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] + public Optional AllowDMUsage { get; set; } - [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultMemberPermissions { get; set; } + [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultMemberPermissions { get; set; } - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public Optional NSFW { get; set; } - } + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public Optional NSFW { get; set; } +} - internal class RestInteractionResponsePayload - { - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public InteractionResponseType Type { get; set; } +internal class RestInteractionResponsePayload +{ + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public InteractionResponseType Type { get; set; } - [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInteractionApplicationCommandCallbackData Data { get; set; } - } + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInteractionApplicationCommandCallbackData? Data { get; set; } +} - internal class RestFollowupMessageCreatePayload - { - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public string Content { get; set; } +internal class RestFollowupMessageCreatePayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string? Content { get; set; } - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; set; } + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; set; } - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Embeds { get; set; } + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Embeds { get; set; } - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions Mentions { get; set; } + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions? Mentions { get; set; } - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public int? Flags { get; set; } + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public int? Flags { get; set; } - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyCollection Components { get; set; } - } + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyCollection Components { get; set; } +} - internal class RestEditApplicationCommandPermissionsPayload - { - [JsonProperty("permissions")] - public IEnumerable Permissions { get; set; } - } +internal class RestEditApplicationCommandPermissionsPayload +{ + [JsonProperty("permissions")] + public IEnumerable Permissions { get; set; } } diff --git a/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs index 469cad3323..3e012001c8 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs @@ -3,319 +3,320 @@ using DSharpPlus.Entities; using Newtonsoft.Json; -namespace DSharpPlus.Net.Abstractions +namespace DSharpPlus.Net.Abstractions; + +internal sealed class RestChannelCreatePayload { - internal sealed class RestChannelCreatePayload - { - [JsonProperty("name")] - public string Name { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public ChannelType Type { get; set; } + + [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? Parent { get; set; } + + [JsonProperty("topic")] + public Optional Topic { get; set; } + + [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] + public int? Bitrate { get; set; } - [JsonProperty("type")] - public ChannelType Type { get; set; } + [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? UserLimit { get; set; } - [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? Parent { get; set; } + [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? PermissionOverwrites { get; set; } - [JsonProperty("topic")] - public Optional Topic { get; set; } + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? Nsfw { get; set; } - [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] - public int? Bitrate { get; set; } + [JsonProperty("rate_limit_per_user")] + public Optional PerUserRateLimit { get; set; } - [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? UserLimit { get; set; } + [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] + public VideoQualityMode? QualityMode { get; set; } - [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable PermissionOverwrites { get; set; } + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int? Position { get; set; } - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? Nsfw { get; set; } + [JsonProperty("default_auth_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public AutoArchiveDuration? DefaultAutoArchiveDuration { get; set; } + + [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] + public DefaultReaction? DefaultReaction { get; set; } + + [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AvailableTags { get; set; } + + [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] + public DefaultSortOrder? DefaultSortOrder { get; set; } +} - [JsonProperty("rate_limit_per_user")] - public Optional PerUserRateLimit { get; set; } +internal sealed class RestChannelModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public required string Name { get; set; } - [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] - public VideoQualityMode? QualityMode { get; set; } + [JsonProperty("type")] + public Optional Type { get; set; } - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int? Position { get; set; } + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int? Position { get; set; } - [JsonProperty("default_auth_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public AutoArchiveDuration? DefaultAutoArchiveDuration { get; set; } + [JsonProperty("topic")] + public Optional Topic { get; set; } - [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] - public DefaultReaction? DefaultReaction { get; set; } + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? Nsfw { get; set; } - [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable AvailableTags { get; set; } + [JsonProperty("parent_id")] + public Optional Parent { get; set; } - [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] - public DefaultSortOrder? DefaultSortOrder { get; set; } - } + [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] + public int? Bitrate { get; set; } - internal sealed class RestChannelModifyPayload - { - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } + [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? UserLimit { get; set; } - [JsonProperty("type")] - public Optional Type { get; set; } + [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? PermissionOverwrites { get; set; } - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int? Position { get; set; } + [JsonProperty("rate_limit_per_user")] + public Optional PerUserRateLimit { get; set; } - [JsonProperty("topic")] - public Optional Topic { get; set; } + [JsonProperty("rtc_region")] + public Optional RtcRegion { get; set; } - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? Nsfw { get; set; } + [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] + public VideoQualityMode? QualityMode { get; set; } - [JsonProperty("parent_id")] - public Optional Parent { get; set; } + [JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultAutoArchiveDuration { get; set; } - [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] - public int? Bitrate { get; set; } + [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultSortOrder { get; set; } - [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? UserLimit { get; set; } + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public Optional Flags { get; set; } - [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable PermissionOverwrites { get; set; } + [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultReaction { get; set; } - [JsonProperty("rate_limit_per_user")] - public Optional PerUserRateLimit { get; set; } + [JsonProperty("default_per_user_rate_limit", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultPerUserRateLimit { get; set; } - [JsonProperty("rtc_region")] - public Optional RtcRegion { get; set; } + [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AvailableTags { get; set; } - [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] - public VideoQualityMode? QualityMode { get; set; } + [JsonProperty("default_forum_layout", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultForumLayout { get; set; } +} - [JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultAutoArchiveDuration { get; set; } +internal sealed class RestThreadChannelModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public required string Name { get; set; } - [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultSortOrder { get; set; } + [JsonProperty("type")] + public Optional Type { get; set; } - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public Optional Flags { get; set; } + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int? Position { get; set; } - [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultReaction { get; set; } + [JsonProperty("topic")] + public Optional Topic { get; set; } - [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable AvailableTags { get; set; } + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? Nsfw { get; set; } - [JsonProperty("default_forum_layout", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultForumLayout { get; set; } - } + [JsonProperty("parent_id")] + public Optional Parent { get; set; } - internal sealed class RestThreadChannelModifyPayload - { - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } + [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] + public int? Bitrate { get; set; } - [JsonProperty("type")] - public Optional Type { get; set; } + [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? UserLimit { get; set; } - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int? Position { get; set; } + [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? PermissionOverwrites { get; set; } - [JsonProperty("topic")] - public Optional Topic { get; set; } + [JsonProperty("rate_limit_per_user")] + public Optional PerUserRateLimit { get; set; } - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? Nsfw { get; set; } + [JsonProperty("rtc_region")] + public Optional RtcRegion { get; set; } - [JsonProperty("parent_id")] - public Optional Parent { get; set; } + [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] + public VideoQualityMode? QualityMode { get; set; } - [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] - public int? Bitrate { get; set; } + [JsonProperty("archived", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsArchived { get; set; } - [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? UserLimit { get; set; } + [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public AutoArchiveDuration? ArchiveDuration { get; set; } - [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable PermissionOverwrites { get; set; } + [JsonProperty("locked", NullValueHandling = NullValueHandling.Ignore)] + public bool? Locked { get; set; } - [JsonProperty("rate_limit_per_user")] - public Optional PerUserRateLimit { get; set; } + [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AppliedTags { get; set; } +} - [JsonProperty("rtc_region")] - public Optional RtcRegion { get; set; } +internal class RestChannelMessageEditPayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Include)] + public string? Content { get; set; } - [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] - public VideoQualityMode? QualityMode { get; set; } + [JsonIgnore] + public bool HasContent { get; set; } - [JsonProperty("archived", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsArchived { get; set; } + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Embeds { get; set; } - [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public AutoArchiveDuration? ArchiveDuration { get; set; } + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions? Mentions { get; set; } - [JsonProperty("locked", NullValueHandling = NullValueHandling.Ignore)] - public bool? Locked { get; set; } + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyCollection? Components { get; set; } - [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable AppliedTags { get; set; } - } + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public MessageFlags? Flags { get; set; } - internal class RestChannelMessageEditPayload - { - [JsonProperty("content", NullValueHandling = NullValueHandling.Include)] - public string Content { get; set; } + [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Attachments { get; set; } - [JsonIgnore] - public bool HasContent { get; set; } + [JsonIgnore] + public bool HasEmbed { get; set; } - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Embeds { get; set; } + public bool ShouldSerializeContent() + => this.HasContent; - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions Mentions { get; set; } + public bool ShouldSerializeEmbed() + => this.HasEmbed; +} - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyCollection Components { get; set; } +internal sealed class RestChannelMessageCreatePayload : RestChannelMessageEditPayload +{ + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; set; } - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public MessageFlags? Flags { get; set; } + [JsonProperty("sticker_ids", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? StickersIds { get; set; } // Discord sends an array, but you can only have one* sticker on a message // - [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Attachments { get; set; } + [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] + public InternalDiscordMessageReference? MessageReference { get; set; } +} - [JsonIgnore] - public bool HasEmbed { get; set; } +internal sealed class RestChannelMessageCreateMultipartPayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string Content { get; set; } - public bool ShouldSerializeContent() - => this.HasContent; + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; set; } - public bool ShouldSerializeEmbed() - => this.HasEmbed; - } + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Embeds { get; set; } - internal sealed class RestChannelMessageCreatePayload : RestChannelMessageEditPayload - { - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; set; } + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions Mentions { get; set; } - [JsonProperty("sticker_ids", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable StickersIds { get; set; } // Discord sends an array, but you can only have one* sticker on a message // + [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] + public InternalDiscordMessageReference? MessageReference { get; set; } +} - [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] - public InternalDiscordMessageReference? MessageReference { get; set; } +internal sealed class RestChannelMessageBulkDeletePayload +{ + [JsonProperty("messages", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Messages { get; set; } +} - } +internal sealed class RestChannelMessageSuppressEmbedsPayload +{ + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Suppress { get; set; } +} - internal sealed class RestChannelMessageCreateMultipartPayload - { - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public string Content { get; set; } +internal sealed class RestChannelInviteCreatePayload +{ + [JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)] + public int MaxAge { get; set; } - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; set; } + [JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)] + public int MaxUses { get; set; } - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Embeds { get; set; } + [JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)] + public bool Temporary { get; set; } - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions Mentions { get; set; } + [JsonProperty("unique", NullValueHandling = NullValueHandling.Ignore)] + public bool Unique { get; set; } - [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] - public InternalDiscordMessageReference? MessageReference { get; set; } - } + [JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)] + public InviteTargetType? TargetType { get; set; } - internal sealed class RestChannelMessageBulkDeletePayload - { - [JsonProperty("messages", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Messages { get; set; } - } + [JsonProperty("target_user_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? TargetUserId { get; set; } - internal sealed class RestChannelMessageSuppressEmbedsPayload - { - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Suppress { get; set; } - } + [JsonProperty("target_application_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? TargetApplicationId { get; set; } +} - internal sealed class RestChannelInviteCreatePayload - { - [JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)] - public int MaxAge { get; set; } +internal sealed class RestChannelPermissionEditPayload +{ + [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] + public Permissions Allow { get; set; } - [JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)] - public int MaxUses { get; set; } + [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] + public Permissions Deny { get; set; } - [JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)] - public bool Temporary { get; set; } + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } +} - [JsonProperty("unique", NullValueHandling = NullValueHandling.Ignore)] - public bool Unique { get; set; } +internal sealed class RestChannelGroupDmRecipientAddPayload : IOAuth2Payload +{ + [JsonProperty("access_token")] + public string AccessToken { get; set; } - [JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)] - public InviteTargetType? TargetType { get; set; } + [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] + public string Nickname { get; set; } +} - [JsonProperty("target_user_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? TargetUserId { get; set; } +internal sealed class AcknowledgePayload +{ + [JsonProperty("token", NullValueHandling = NullValueHandling.Include)] + public string Token { get; set; } +} - [JsonProperty("target_application_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? TargetApplicationId { get; set; } - } +internal sealed class RestCreateStageInstancePayload +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } - internal sealed class RestChannelPermissionEditPayload - { - [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] - public Permissions Allow { get; set; } + [JsonProperty("topic")] + public string Topic { get; set; } - [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] - public Permissions Deny { get; set; } + [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] + public PrivacyLevel? PrivacyLevel { get; set; } +} - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public string Type { get; set; } - } +internal sealed class RestModifyStageInstancePayload +{ + [JsonProperty("topic")] + public Optional Topic { get; set; } - internal sealed class RestChannelGroupDmRecipientAddPayload : IOAuth2Payload - { - [JsonProperty("access_token")] - public string AccessToken { get; set; } + [JsonProperty("privacy_level")] + public Optional PrivacyLevel { get; set; } +} - [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] - public string Nickname { get; set; } - } - - internal sealed class AcknowledgePayload - { - [JsonProperty("token", NullValueHandling = NullValueHandling.Include)] - public string Token { get; set; } - } - - internal sealed class RestCreateStageInstancePayload - { - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - - [JsonProperty("topic")] - public string Topic { get; set; } - - [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] - public PrivacyLevel? PrivacyLevel { get; set; } - } - - internal sealed class RestModifyStageInstancePayload - { - [JsonProperty("topic")] - public Optional Topic { get; set; } - - [JsonProperty("privacy_level")] - public Optional PrivacyLevel { get; set; } - } - - internal sealed class RestBecomeStageSpeakerInstancePayload - { - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTime? RequestToSpeakTimestamp { get; set; } - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Suppress { get; set; } - } +internal sealed class RestBecomeStageSpeakerInstancePayload +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTime? RequestToSpeakTimestamp { get; set; } + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Suppress { get; set; } } diff --git a/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs index 35fde76059..0bd0b2e57f 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs @@ -3,361 +3,362 @@ using DSharpPlus.Entities; using Newtonsoft.Json; -namespace DSharpPlus.Net.Abstractions +namespace DSharpPlus.Net.Abstractions; + +internal interface IReasonAction +{ + string Reason { get; set; } + + //[JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] + //public string Reason { get; set; } +} + +internal class RestGuildCreatePayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] + public string RegionId { get; set; } + + [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] + public Optional IconBase64 { get; set; } + + [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] + public VerificationLevel? VerificationLevel { get; set; } + + [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] + public DefaultMessageNotifications? DefaultMessageNotifications { get; set; } + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Roles { get; set; } + + [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Channels { get; set; } + + [JsonProperty("system_channel_flags", NullValueHandling = NullValueHandling.Ignore)] + public SystemChannelFlags? SystemChannelFlags { get; set; } +} + +internal sealed class RestGuildCreateFromTemplatePayload { - internal interface IReasonAction - { - string Reason { get; set; } + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] + public Optional IconBase64 { get; set; } +} - //[JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] - //public string Reason { get; set; } - } +internal sealed class RestGuildModifyPayload +{ + [JsonProperty("name")] + public Optional Name { get; set; } - internal class RestGuildCreatePayload - { - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } + [JsonProperty("region")] + public Optional RegionId { get; set; } - [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] - public string RegionId { get; set; } + [JsonProperty("icon")] + public Optional IconBase64 { get; set; } - [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] - public Optional IconBase64 { get; set; } + [JsonProperty("verification_level")] + public Optional VerificationLevel { get; set; } - [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] - public VerificationLevel? VerificationLevel { get; set; } + [JsonProperty("default_message_notifications")] + public Optional DefaultMessageNotifications { get; set; } - [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] - public DefaultMessageNotifications? DefaultMessageNotifications { get; set; } + [JsonProperty("owner_id")] + public Optional OwnerId { get; set; } - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Roles { get; set; } + [JsonProperty("splash")] + public Optional SplashBase64 { get; set; } - [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Channels { get; set; } + [JsonProperty("afk_channel_id")] + public Optional AfkChannelId { get; set; } - [JsonProperty("system_channel_flags", NullValueHandling = NullValueHandling.Ignore)] - public SystemChannelFlags? SystemChannelFlags { get; set; } - } + [JsonProperty("afk_timeout")] + public Optional AfkTimeout { get; set; } - internal sealed class RestGuildCreateFromTemplatePayload - { - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } + [JsonProperty("mfa_level")] + public Optional MfaLevel { get; set; } - [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] - public Optional IconBase64 { get; set; } - } + [JsonProperty("explicit_content_filter")] + public Optional ExplicitContentFilter { get; set; } - internal sealed class RestGuildModifyPayload - { - [JsonProperty("name")] - public Optional Name { get; set; } + [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] + public Optional SystemChannelId { get; set; } - [JsonProperty("region")] - public Optional RegionId { get; set; } + [JsonProperty("banner")] + public Optional Banner { get; set; } - [JsonProperty("icon")] - public Optional IconBase64 { get; set; } + [JsonProperty("discorvery_splash")] + public Optional DiscoverySplash { get; set; } - [JsonProperty("verification_level")] - public Optional VerificationLevel { get; set; } + [JsonProperty("system_channel_flags")] + public Optional SystemChannelFlags { get; set; } - [JsonProperty("default_message_notifications")] - public Optional DefaultMessageNotifications { get; set; } + [JsonProperty("rules_channel_id")] + public Optional RulesChannelId { get; set; } - [JsonProperty("owner_id")] - public Optional OwnerId { get; set; } + [JsonProperty("public_updates_channel_id")] + public Optional PublicUpdatesChannelId { get; set; } - [JsonProperty("splash")] - public Optional SplashBase64 { get; set; } + [JsonProperty("preferred_locale")] + public Optional PreferredLocale { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("features")] + public Optional> Features { get; set; } +} - [JsonProperty("afk_channel_id")] - public Optional AfkChannelId { get; set; } +internal sealed class RestGuildMemberAddPayload : IOAuth2Payload +{ + [JsonProperty("access_token")] + public string AccessToken { get; set; } - [JsonProperty("afk_timeout")] - public Optional AfkTimeout { get; set; } + [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] + public string Nickname { get; set; } - [JsonProperty("mfa_level")] - public Optional MfaLevel { get; set; } + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Roles { get; set; } - [JsonProperty("explicit_content_filter")] - public Optional ExplicitContentFilter { get; set; } + [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] + public bool? Mute { get; set; } - [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] - public Optional SystemChannelId { get; set; } + [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] + public bool? Deaf { get; set; } +} - [JsonProperty("banner")] - public Optional Banner { get; set; } +internal sealed class RestScheduledGuildEventCreatePayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } - [JsonProperty("discorvery_splash")] - public Optional DiscoverySplash { get; set; } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } - [JsonProperty("system_channel_flags")] - public Optional SystemChannelFlags { get; set; } + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ChannelId { get; set; } - [JsonProperty("rules_channel_id")] - public Optional RulesChannelId { get; set; } + [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] + public ScheduledGuildEventPrivacyLevel PrivacyLevel { get; set; } - [JsonProperty("public_updates_channel_id")] - public Optional PublicUpdatesChannelId { get; set; } + [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] + public ScheduledGuildEventType Type { get; set; } - [JsonProperty("preferred_locale")] - public Optional PreferredLocale { get; set; } + [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset StartTime { get; set; } - [JsonProperty("description")] - public Optional Description { get; set; } + [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)]// Null = no end date + public DateTimeOffset? EndTime { get; set; } - [JsonProperty("features")] - public Optional> Features { get; set; } - } + [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] + public DiscordScheduledGuildEventMetadata? Metadata { get; set; } +} - internal sealed class RestGuildMemberAddPayload : IOAuth2Payload - { - [JsonProperty("access_token")] - public string AccessToken { get; set; } +internal sealed class RestScheduledGuildEventModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public Optional Name { get; set; } - [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] - public string Nickname { get; set; } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public Optional Description { get; set; } - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Roles { get; set; } + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public Optional ChannelId { get; set; } - [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] - public bool? Mute { get; set; } + [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] + public Optional PrivacyLevel { get; set; } - [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] - public bool? Deaf { get; set; } - } + [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] + public Optional Type { get; set; } - internal sealed class RestScheduledGuildEventCreatePayload - { - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } + [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] + public Optional StartTime { get; set; } - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } + [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] + public Optional EndTime { get; set; } - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ChannelId { get; set; } + [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] + public Optional Metadata { get; set; } - [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] - public ScheduledGuildEventPrivacyLevel PrivacyLevel { get; set; } + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public Optional Status { get; set; } +} - [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] - public ScheduledGuildEventType Type { get; set; } +internal sealed class RestGuildChannelReorderPayload +{ + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ChannelId { get; set; } - [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset StartTime { get; set; } + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int Position { get; set; } - [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)]// Null = no end date - public DateTimeOffset? EndTime { get; set; } + [JsonProperty("lock_permissions", NullValueHandling = NullValueHandling.Ignore)] + public bool? LockPermissions { get; set; } - [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] - public DiscordScheduledGuildEventMetadata Metadata { get; set; } - } + [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ParentId { get; set; } +} - internal sealed class RestScheduledGuildEventModifyPayload - { - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public Optional Name { get; set; } +internal sealed class RestGuildRoleReorderPayload +{ + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong RoleId { get; set; } - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public Optional Description { get; set; } + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int Position { get; set; } +} - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public Optional ChannelId { get; set; } +internal sealed class RestGuildMemberModifyPayload +{ + [JsonProperty("nick")] + public Optional Nickname { get; set; } - [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] - public Optional PrivacyLevel { get; set; } + [JsonProperty("roles")] + public Optional> RoleIds { get; set; } - [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] - public Optional Type { get; set; } + [JsonProperty("mute")] + public Optional Mute { get; set; } - [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] - public Optional StartTime { get; set; } + [JsonProperty("deaf")] + public Optional Deafen { get; set; } - [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] - public Optional EndTime { get; set; } + [JsonProperty("channel_id")] + public Optional VoiceChannelId { get; set; } - [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] - public Optional Metadata { get; set; } + [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] + public Optional CommunicationDisabledUntil { get; set; } +} - [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] - public Optional Status { get; set; } - } +internal sealed class RestGuildRolePayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } - internal sealed class RestGuildChannelReorderPayload - { - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ChannelId { get; set; } + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public Permissions? Permissions { get; set; } - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int Position { get; set; } + [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] + public int? Color { get; set; } - [JsonProperty("lock_permissions", NullValueHandling = NullValueHandling.Ignore)] - public bool? LockPermissions { get; set; } + [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] + public bool? Hoist { get; set; } - [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ParentId { get; set; } - } + [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] + public bool? Mentionable { get; set; } - internal sealed class RestGuildRoleReorderPayload - { - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong RoleId { get; set; } + [JsonProperty("unicode_emoji", NullValueHandling = NullValueHandling.Ignore)] + public string? Emoji { get; set; } - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int Position { get; set; } - } + [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] + public string? Icon { get; set; } +} - internal sealed class RestGuildMemberModifyPayload - { - [JsonProperty("nick")] - public Optional Nickname { get; set; } +internal sealed class RestGuildPruneResultPayload +{ + [JsonProperty("pruned", NullValueHandling = NullValueHandling.Ignore)] + public int? Pruned { get; set; } +} - [JsonProperty("roles")] - public Optional> RoleIds { get; set; } +internal sealed class RestGuildIntegrationAttachPayload +{ + [JsonProperty("type")] + public string Type { get; set; } - [JsonProperty("mute")] - public Optional Mute { get; set; } + [JsonProperty("id")] + public ulong Id { get; set; } +} - [JsonProperty("deaf")] - public Optional Deafen { get; set; } +internal sealed class RestGuildIntegrationModifyPayload +{ + [JsonProperty("expire_behavior", NullValueHandling = NullValueHandling.Ignore)] + public int? ExpireBehavior { get; set; } - [JsonProperty("channel_id")] - public Optional VoiceChannelId { get; set; } + [JsonProperty("expire_grace_period", NullValueHandling = NullValueHandling.Ignore)] + public int? ExpireGracePeriod { get; set; } - [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] - public Optional CommunicationDisabledUntil { get; set; } - } + [JsonProperty("enable_emoticons", NullValueHandling = NullValueHandling.Ignore)] + public bool? EnableEmoticons { get; set; } +} - internal sealed class RestGuildRolePayload - { - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } +internal class RestGuildEmojiModifyPayload +{ + [JsonProperty("name")] + public string? Name { get; set; } - [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] - public Permissions? Permissions { get; set; } + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public ulong[]? Roles { get; set; } +} - [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] - public int? Color { get; set; } +internal class RestGuildEmojiCreatePayload : RestGuildEmojiModifyPayload +{ + [JsonProperty("image")] + public string? ImageB64 { get; set; } +} - [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] - public bool? Hoist { get; set; } +internal class RestGuildWidgetSettingsPayload +{ + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool? Enabled { get; set; } - [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] - public bool? Mentionable { get; set; } + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ChannelId { get; set; } +} - [JsonProperty("unicode_emoji", NullValueHandling = NullValueHandling.Ignore)] - public string Emoji { get; set; } +// TODO: this is wrong. i've annotated them for now, but we'll need to use optionals here +// since optional/nullable mean two different things in the context of modifying. +internal class RestGuildTemplateCreateOrModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] + public string? Name { get; set; } - [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] - public string Icon { get; set; } - } + [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] + public string? Description { get; set; } +} - internal sealed class RestGuildPruneResultPayload - { - [JsonProperty("pruned", NullValueHandling = NullValueHandling.Ignore)] - public int? Pruned { get; set; } - } +internal class RestGuildMembershipScreeningFormModifyPayload +{ + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public Optional Enabled { get; set; } - internal sealed class RestGuildIntegrationAttachPayload - { - [JsonProperty("type")] - public string Type { get; set; } + [JsonProperty("form_fields", NullValueHandling = NullValueHandling.Ignore)] + public Optional Fields { get; set; } - [JsonProperty("id")] - public ulong Id { get; set; } - } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public Optional Description { get; set; } +} - internal sealed class RestGuildIntegrationModifyPayload - { - [JsonProperty("expire_behavior", NullValueHandling = NullValueHandling.Ignore)] - public int? ExpireBehavior { get; set; } +internal class RestGuildWelcomeScreenModifyPayload +{ + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public Optional Enabled { get; set; } - [JsonProperty("expire_grace_period", NullValueHandling = NullValueHandling.Ignore)] - public int? ExpireGracePeriod { get; set; } + [JsonProperty("welcome_channels", NullValueHandling = NullValueHandling.Ignore)] + public Optional> WelcomeChannels { get; set; } - [JsonProperty("enable_emoticons", NullValueHandling = NullValueHandling.Ignore)] - public bool? EnableEmoticons { get; set; } - } + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public Optional Description { get; set; } +} - internal class RestGuildEmojiModifyPayload - { - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public ulong[] Roles { get; set; } - } +internal class RestGuildUpdateCurrentUserVoiceStatePayload +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } - internal class RestGuildEmojiCreatePayload : RestGuildEmojiModifyPayload - { - [JsonProperty("image")] - public string ImageB64 { get; set; } - } + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Suppress { get; set; } - internal class RestGuildWidgetSettingsPayload - { - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool? Enabled { get; set; } - - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ChannelId { get; set; } - } + [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? RequestToSpeakTimestamp { get; set; } +} - internal class RestGuildTemplateCreateOrModifyPayload - { - [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] - public string Name { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] - public string Description { get; set; } - } +internal class RestGuildUpdateUserVoiceStatePayload +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } - internal class RestGuildMembershipScreeningFormModifyPayload - { - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public Optional Enabled { get; set; } - - [JsonProperty("form_fields", NullValueHandling = NullValueHandling.Ignore)] - public Optional Fields { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public Optional Description { get; set; } - } - - internal class RestGuildWelcomeScreenModifyPayload - { - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public Optional Enabled { get; set; } - - [JsonProperty("welcome_channels", NullValueHandling = NullValueHandling.Ignore)] - public Optional> WelcomeChannels { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public Optional Description { get; set; } - } - - internal class RestGuildUpdateCurrentUserVoiceStatePayload - { - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Suppress { get; set; } - - [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? RequestToSpeakTimestamp { get; set; } - } - - internal class RestGuildUpdateUserVoiceStatePayload - { - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Suppress { get; set; } - } + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Suppress { get; set; } } diff --git a/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs index 4f198199cf..ea1ab48aec 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs @@ -1,37 +1,34 @@ using System.Collections.Generic; -using DSharpPlus.Entities; using Newtonsoft.Json; -namespace DSharpPlus.Net.Abstractions -{ - internal sealed class RestThreadCreatePayload - { - [JsonProperty("name")] - public string Name { get; set; } +namespace DSharpPlus.Net.Abstractions; - [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public AutoArchiveDuration ArchiveAfter { get; set; } +internal sealed class RestThreadCreatePayload +{ + [JsonProperty("name")] + public required string Name { get; set; } - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public ChannelType? Type { get; set; } - } + [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public AutoArchiveDuration ArchiveAfter { get; set; } - internal sealed class RestForumPostCreatePayload - { - [JsonProperty("name")] - public string Name { get; set; } + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public ChannelType? Type { get; set; } +} - [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public AutoArchiveDuration? ArchiveAfter { get; set; } +internal sealed class RestForumPostCreatePayload +{ + [JsonProperty("name")] + public required string Name { get; set; } - [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Include)] - public int? RateLimitPerUser { get; set; } + [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public AutoArchiveDuration? ArchiveAfter { get; set; } - [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] - public RestChannelMessageCreatePayload Message { get; set; } + [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Include)] + public int? RateLimitPerUser { get; set; } - [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable AppliedTags { get; set; } - } + [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] + public required RestChannelMessageCreatePayload Message { get; set; } + [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AppliedTags { get; set; } } diff --git a/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs index 6e55357686..174d36902d 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs @@ -1,65 +1,64 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace DSharpPlus.Net.Abstractions +namespace DSharpPlus.Net.Abstractions; + +internal sealed class RestUserDmCreatePayload { - internal sealed class RestUserDmCreatePayload - { - [JsonProperty("recipient_id")] - public ulong Recipient { get; set; } - } + [JsonProperty("recipient_id")] + public ulong Recipient { get; set; } +} - internal sealed class RestUserGroupDmCreatePayload - { - [JsonProperty("access_tokens")] - public IEnumerable AccessTokens { get; set; } +internal sealed class RestUserGroupDmCreatePayload +{ + [JsonProperty("access_tokens")] + public IEnumerable? AccessTokens { get; set; } - [JsonProperty("nicks")] - public IDictionary Nicknames { get; set; } - } + [JsonProperty("nicks")] + public IDictionary? Nicknames { get; set; } +} - internal sealed class RestUserUpdateCurrentPayload - { - [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] - public string Username { get; set; } +internal sealed class RestUserUpdateCurrentPayload +{ + [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] + public string? Username { get; set; } - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] - public string AvatarBase64 { get; set; } + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] + public string? AvatarBase64 { get; set; } - [JsonIgnore] - public bool AvatarSet { get; set; } + [JsonIgnore] + public bool AvatarSet { get; set; } - public bool ShouldSerializeAvatarBase64() - => this.AvatarSet; - } + public bool ShouldSerializeAvatarBase64() + => this.AvatarSet; +} - internal sealed class RestUserGuild - { - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public ulong Id { get; set; } +internal sealed class RestUserGuild +{ + [JsonProperty("id")] + public ulong Id { get; set; } - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public string IconHash { get; set; } + [JsonProperty("icon_hash", NullValueHandling = NullValueHandling.Ignore)] + public string? IconHash { get; set; } - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public bool IsOwner { get; set; } + [JsonProperty("is_owner", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsOwner { get; set; } - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public Permissions Permissions { get; set; } - } + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public Permissions Permissions { get; set; } +} - internal sealed class RestUserGuildListPayload - { - [JsonProperty("limit", NullValueHandling = NullValueHandling.Ignore)] - public int Limit { get; set; } +internal sealed class RestUserGuildListPayload +{ + [JsonProperty("limit", NullValueHandling = NullValueHandling.Ignore)] + public int Limit { get; set; } - [JsonProperty("before", NullValueHandling = NullValueHandling.Ignore)] - public ulong? Before { get; set; } + [JsonProperty("before", NullValueHandling = NullValueHandling.Ignore)] + public ulong? Before { get; set; } - [JsonProperty("after", NullValueHandling = NullValueHandling.Ignore)] - public ulong? After { get; set; } - } + [JsonProperty("after", NullValueHandling = NullValueHandling.Ignore)] + public ulong? After { get; set; } } diff --git a/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs index 956ef278b2..a262e31135 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs @@ -2,64 +2,64 @@ using DSharpPlus.Entities; using Newtonsoft.Json; -namespace DSharpPlus.Net.Abstractions +namespace DSharpPlus.Net.Abstractions; + +internal sealed class RestWebhookPayload { - internal sealed class RestWebhookPayload - { - [JsonProperty("name")] - public string Name { get; set; } + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } + + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] + public string? AvatarBase64 { get; set; } - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] - public string AvatarBase64 { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } + [JsonProperty] + public bool AvatarSet { get; set; } - [JsonProperty] - public bool AvatarSet { get; set; } + public bool ShouldSerializeAvatarBase64() + => this.AvatarSet; +} - public bool ShouldSerializeAvatarBase64() - => this.AvatarSet; - } +internal sealed class RestWebhookExecutePayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string? Content { get; set; } - internal sealed class RestWebhookExecutePayload - { - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public string Content { get; set; } + [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] + public string? Username { get; set; } - [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] - public string Username { get; set; } + [JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)] + public string? AvatarUrl { get; set; } - [JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)] - public string AvatarUrl { get; set; } + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; set; } - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; set; } + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Embeds { get; set; } - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Embeds { get; set; } - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Components { get; set; } + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Components { get; set; } - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions Mentions { get; set; } - } + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions? Mentions { get; set; } +} - internal sealed class RestWebhookMessageEditPayload - { - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public Optional Content { get; set; } +internal sealed class RestWebhookMessageEditPayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public Optional Content { get; set; } - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Embeds { get; set; } + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Embeds { get; set; } - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions Mentions { get; set; } + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions? Mentions { get; set; } - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Components { get; set; } + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Components { get; set; } - [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Attachments { get; set; } - } + [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Attachments { get; set; } } diff --git a/DSharpPlus/Net/Rest/BaseRestRequest.cs b/DSharpPlus/Net/Rest/BaseRestRequest.cs deleted file mode 100644 index ec3f306694..0000000000 --- a/DSharpPlus/Net/Rest/BaseRestRequest.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace DSharpPlus.Net -{ - /// - /// Represents a request sent over HTTP. - /// - public abstract class BaseRestRequest - { - protected internal BaseDiscordClient Discord { get; } - protected internal TaskCompletionSource RequestTaskSource { get; } - - /// - /// Gets the url to which this request is going to be made. - /// - public Uri Url { get; } - - /// - /// Gets the HTTP method used for this request. - /// - public RestRequestMethod Method { get; } - - /// - /// Gets the generic path (no parameters) for this request. - /// - public string Route { get; } - - /// - /// Gets the headers sent with this request. - /// - public IReadOnlyDictionary Headers { get; } = null; - - /// - /// Gets the override for the rate limit bucket wait time. - /// - public double? RateLimitWaitOverride { get; } - - /// - /// Gets the rate limit bucket this request is in. - /// - internal RateLimitBucket RateLimitBucket { get; } - - /// - /// Creates a new with specified parameters. - /// - /// from which this request originated. - /// Rate limit bucket to place this request in. - /// Uri to which this request is going to be sent to. - /// Method to use for this request, - /// The generic route the request url will use. - /// Additional headers for this request. - /// Override for ratelimit bucket wait time. - internal BaseRestRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, double? ratelimitWaitOverride = null) - { - this.Discord = client; - this.RateLimitBucket = bucket; - this.RequestTaskSource = new TaskCompletionSource(); - this.Url = url; - this.Method = method; - this.Route = route; - this.RateLimitWaitOverride = ratelimitWaitOverride; - - if (headers != null) - { - headers = headers.Select(x => new KeyValuePair(x.Key, Uri.EscapeDataString(x.Value))) - .ToDictionary(x => x.Key, x => x.Value); - this.Headers = headers; - } - } - - /// - /// Asynchronously waits for this request to complete. - /// - /// HTTP response to this request. - public Task WaitForCompletionAsync() - => this.RequestTaskSource.Task; - - protected internal void SetCompleted(RestResponse response) - => this.RequestTaskSource.SetResult(response); - - protected internal void SetFaulted(Exception ex) - => this.RequestTaskSource.SetException(ex); - - protected internal bool TrySetFaulted(Exception ex) - => this.RequestTaskSource.TrySetException(ex); - - } -} diff --git a/DSharpPlus/Net/Rest/DiscordApiClient.cs b/DSharpPlus/Net/Rest/DiscordApiClient.cs index 89dc2e4605..6d96ab6b10 100644 --- a/DSharpPlus/Net/Rest/DiscordApiClient.cs +++ b/DSharpPlus/Net/Rest/DiscordApiClient.cs @@ -5,3252 +5,5166 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; using System.Threading.Tasks; + using DSharpPlus.Entities; +using DSharpPlus.Entities.AuditLogs; using DSharpPlus.Enums; using DSharpPlus.Net.Abstractions; using DSharpPlus.Net.Serialization; + using Microsoft.Extensions.Logging; + using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace DSharpPlus.Net +namespace DSharpPlus.Net; + +// huge credits to dvoraks 8th symphony for being a source of sanity in the trying times of +// fixing this absolute catastrophy up at least somewhat + +public sealed class DiscordApiClient { - public sealed class DiscordApiClient + private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; + + internal BaseDiscordClient? _discord { get; } + internal RestClient _rest { get; } + + internal DiscordApiClient(BaseDiscordClient client) { - private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; + this._discord = client; + this._rest = new RestClient(client); + } - internal BaseDiscordClient _discord { get; } - internal RestClient _rest { get; } + internal DiscordApiClient + ( + IWebProxy proxy, + TimeSpan timeout, + ILogger logger + ) // This is for meta-clients, such as the webhook client + => this._rest = new(proxy, timeout, logger); - internal DiscordApiClient(BaseDiscordClient client) - { - this._discord = client; - this._rest = new RestClient(client); - } + private DiscordMessage PrepareMessage(JToken msgRaw) + { + TransportUser author = msgRaw["author"]!.ToDiscordObject(); + DiscordMessage message = msgRaw.ToDiscordObject(); - internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client - { - this._rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger); - } + message.Discord = this._discord!; - private static string BuildQueryString(IDictionary values, bool post = false) - { - if (values == null || values.Count == 0) - return string.Empty; + this.PopulateMessage(author, message); - var vals_collection = values.Select(xkvp => - $"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}"); - var vals = string.Join("&", vals_collection); + JToken? referencedMsg = msgRaw["referenced_message"]; - return !post ? $"?{vals}" : vals; + if (message.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) + { + TransportUser referencedAuthor = referencedMsg["author"]!.ToDiscordObject(); + message.ReferencedMessage.Discord = this._discord!; + this.PopulateMessage(referencedAuthor, message.ReferencedMessage); } - private DiscordMessage PrepareMessage(JToken msg_raw) + if (message.Channel is not null) { - var author = msg_raw["author"].ToDiscordObject(); - var ret = msg_raw.ToDiscordObject(); - ret.Discord = this._discord; - - this.PopulateMessage(author, ret); + return message; + } - var referencedMsg = msg_raw["referenced_message"]; - if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) + message.Channel = !message._guildId.HasValue + ? new DiscordDmChannel { - author = referencedMsg["author"].ToDiscordObject(); - ret.ReferencedMessage.Discord = this._discord; - this.PopulateMessage(author, ret.ReferencedMessage); + Id = message.ChannelId, + Discord = this._discord!, + Type = ChannelType.Private } + : new DiscordChannel + { + Id = message.ChannelId, + GuildId = message._guildId, + Discord = this._discord! + }; - if (ret.Channel != null) - return ret; + return message; + } - var channel = !ret._guildId.HasValue - ? new DiscordDmChannel - { - Id = ret.ChannelId, - Discord = this._discord, - Type = ChannelType.Private - } - : new DiscordChannel - { - Id = ret.ChannelId, - GuildId = ret._guildId, - Discord = this._discord - }; - ret.Channel = channel; + private void PopulateMessage(TransportUser author, DiscordMessage ret) + { + DiscordGuild? guild = ret.Channel?.Guild; - return ret; + //If this is a webhook, it shouldn't be in the user cache. + if (author.IsBot && int.Parse(author.Discriminator) == 0) + { + ret.Author = new(author) + { + Discord = this._discord! + }; } - - private void PopulateMessage(TransportUser author, DiscordMessage ret) + else { - var guild = ret.Channel?.Guild; - - //If this is a webhook, it shouldn't be in the user cache. - if (author.IsBot && author.Discriminator == "0000") + // get and cache the user + if (!this._discord!.UserCache.TryGetValue(author.Id, out DiscordUser? user)) { - ret.Author = new DiscordUser(author) { Discord = this._discord }; + user = new DiscordUser(author) + { + Discord = this._discord + }; } - else + + this._discord.UserCache[author.Id] = user; + + // get the member object if applicable, if not set the message author to an user + if (guild is not null) { - if (!this._discord.UserCache.TryGetValue(author.Id, out var usr)) + if (!guild.Members.TryGetValue(author.Id, out DiscordMember? member)) { - this._discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this._discord }; + member = new(user) + { + Discord = this._discord, + _guild_id = guild.Id + }; } - if (guild != null) - { - if (!guild.Members.TryGetValue(author.Id, out var mbr)) - mbr = new DiscordMember(usr) { Discord = this._discord, _guild_id = guild.Id }; - ret.Author = mbr; - } - else - { - ret.Author = usr; - } + ret.Author = member; + } + else + { + ret.Author = user!; } + } - ret.PopulateMentions(); + ret.PopulateMentions(); - if (ret._reactions == null) - ret._reactions = new List(); - foreach (var xr in ret._reactions) - xr.Emoji.Discord = this._discord; + ret._reactions ??= new List(); + foreach (DiscordReaction reaction in ret._reactions) + { + reaction.Emoji.Discord = this._discord!; } + } - private Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null) - { - var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride); + #region Guild - if (this._discord != null) - this._rest.ExecuteRequestAsync(req).LogTaskFault(this._discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); - else - _ = this._rest.ExecuteRequestAsync(req); + internal async ValueTask> SearchMembersAsync + ( + ulong guildId, + string name, + int? limit = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.SEARCH}"); + builder.AddParameter("query", name); - return req.WaitForCompletionAsync(); + if (limit is not null) + { + builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); } - private Task DoMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null, - IReadOnlyCollection files = null, double? ratelimitWaitOverride = null, bool removeFileCount = false) + RestRequest request = new() { - var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride) - { - _removeFileCount = removeFileCount - }; + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.SEARCH}", + Url = builder.Build(), + Method = HttpMethod.Get + }; + RestResponse response = await this._rest.ExecuteRequestAsync(request); - if (this._discord != null) - this._rest.ExecuteRequestAsync(req).LogTaskFault(this._discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); - else - _ = this._rest.ExecuteRequestAsync(req); - - return req.WaitForCompletionAsync(); - } + JArray array = JArray.Parse(response.Response!); + IReadOnlyList transportMembers = array.ToDiscordObject>(); - #region Guild + List members = new(); - internal async Task> SearchMembersAsync(ulong guild_id, string name, int? limit) + foreach (TransportMember transport in transportMembers) { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var querydict = new Dictionary - { - ["query"] = name, - ["limit"] = limit.ToString() - }; - var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict)); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var json = JArray.Parse(res.Response); - var tms = json.ToDiscordObject>(); - - var mbrs = new List(); - foreach (var xtm in tms) - { - var usr = new DiscordUser(xtm.User) { Discord = this._discord }; - - this._discord.UpdateUserCache(usr); + DiscordUser usr = new(transport.User) { Discord = this._discord! }; - mbrs.Add(new DiscordMember(xtm) { Discord = this._discord, _guild_id = guild_id }); - } + this._discord!.UpdateUserCache(usr); - return mbrs; + members.Add(new DiscordMember(transport) { Discord = this._discord, _guild_id = guildId }); } - internal async Task GetGuildBanAsync(ulong guild_id, ulong user_id) - { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, user_id}, out var url); - var uri = Utilities.GetApiUriFor(url); - var res = await this.DoRequestAsync(this._discord, bucket, uri, RestRequestMethod.GET, route); - var json = JObject.Parse(res.Response); + return members; + } - var ban = json.ToDiscordObject(); + internal async ValueTask GetGuildBanAsync + ( + ulong guildId, + ulong userId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}", + Method = HttpMethod.Get + }; - if (!this._discord.TryGetCachedUserInternal(ban.RawUser.Id, out var usr)) - { - usr = new DiscordUser(ban.RawUser) { Discord = this._discord }; - usr = this._discord.UpdateUserCache(usr); - } + RestResponse response = await this._rest.ExecuteRequestAsync(request); - ban.User = usr; + JObject json = JObject.Parse(response.Response!); - return ban; - } + DiscordBan ban = json.ToDiscordObject(); - internal async Task CreateGuildAsync(string name, string region_id, Optional iconb64, VerificationLevel? verification_level, - DefaultMessageNotifications? default_message_notifications, - SystemChannelFlags? system_channel_flags) + if (!this._discord!.TryGetCachedUserInternal(ban.RawUser.Id, out DiscordUser? user)) { - var pld = new RestGuildCreatePayload - { - Name = name, - RegionId = region_id, - DefaultMessageNotifications = default_message_notifications, - VerificationLevel = verification_level, - IconBase64 = iconb64, - SystemChannelFlags = system_channel_flags - }; - - var route = $"{Endpoints.GUILDS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + user = new DiscordUser(ban.RawUser) { Discord = this._discord }; + user = this._discord.UpdateUserCache(user); + } - var json = JObject.Parse(res.Response); - var raw_members = (JArray)json["members"]; - var guild = json.ToDiscordObject(); + ban.User = user; - if (this._discord is DiscordClient dc) - await dc.OnGuildCreateEventAsync(guild, raw_members, null); - return guild; - } + return ban; + } - internal async Task CreateGuildFromTemplateAsync(string template_code, string name, Optional iconb64) + internal async ValueTask CreateGuildAsync + ( + string name, + string regionId, + Optional iconb64 = default, + VerificationLevel? verificationLevel = null, + DefaultMessageNotifications? defaultMessageNotifications = null, + SystemChannelFlags? systemChannelFlags = null + ) + { + RestGuildCreatePayload payload = new() { - var pld = new RestGuildCreateFromTemplatePayload - { - Name = name, - IconBase64 = iconb64 - }; + Name = name, + RegionId = regionId, + DefaultMessageNotifications = defaultMessageNotifications, + VerificationLevel = verificationLevel, + IconBase64 = iconb64, + SystemChannelFlags = systemChannelFlags + }; - var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { template_code }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}", + Url = $"{Endpoints.GUILDS}", + Payload = DiscordJson.SerializeObject(payload), + Method = HttpMethod.Post + }; - var json = JObject.Parse(res.Response); - var raw_members = (JArray)json["members"]; - var guild = json.ToDiscordObject(); + RestResponse response = await this._rest.ExecuteRequestAsync(request); - if (this._discord is DiscordClient dc) - await dc.OnGuildCreateEventAsync(guild, raw_members, null); - return guild; - } + JObject json = JObject.Parse(response.Response!); + JArray rawMembers = (JArray)json["members"]!; + DiscordGuild guild = json.ToDiscordObject(); - internal async Task DeleteGuildAsync(ulong guild_id) + if (this._discord is DiscordClient dc) { - var route = $"{Endpoints.GUILDS}/:guild_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); + // this looks wrong. TODO: investigate double-fired event? + await dc.OnGuildCreateEventAsync(guild, rawMembers, null!); } - internal async Task ModifyGuildAsync(ulong guildId, Optional name, - Optional region, Optional verificationLevel, - Optional defaultMessageNotifications, Optional mfaLevel, - Optional explicitContentFilter, Optional afkChannelId, - Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64, - Optional systemChannelId, Optional banner, Optional description, - Optional discoverySplash, Optional> features, Optional preferredLocale, - Optional publicUpdatesChannelId, Optional rulesChannelId, Optional systemChannelFlags, - string reason) - { - var pld = new RestGuildModifyPayload - { - Name = name, - RegionId = region, - VerificationLevel = verificationLevel, - DefaultMessageNotifications = defaultMessageNotifications, - MfaLevel = mfaLevel, - ExplicitContentFilter = explicitContentFilter, - AfkChannelId = afkChannelId, - AfkTimeout = afkTimeout, - IconBase64 = iconb64, - SplashBase64 = splashb64, - OwnerId = ownerId, - SystemChannelId = systemChannelId, - Banner = banner, - Description = description, - DiscoverySplash = discoverySplash, - Features = features, - PreferredLocale = preferredLocale, - PublicUpdatesChannelId = publicUpdatesChannelId, - RulesChannelId = rulesChannelId, - SystemChannelFlags = systemChannelFlags - }; + return guild; + } - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + internal async ValueTask CreateGuildFromTemplateAsync + ( + string templateCode, + string name, + Optional iconb64 = default + ) + { + RestGuildCreateFromTemplatePayload payload = new() + { + Name = name, + IconBase64 = iconb64 + }; - var route = $"{Endpoints.GUILDS}/:guild_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/:template_code", + Url = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/{templateCode}", + Payload = DiscordJson.SerializeObject(payload), + Method = HttpMethod.Post + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var json = JObject.Parse(res.Response); - var rawMembers = (JArray)json["members"]; - var guild = json.ToDiscordObject(); - foreach (var r in guild._roles.Values) - r._guild_id = guild.Id; + JObject json = JObject.Parse(res.Response!); + JArray rawMembers = (JArray)json["members"]!; + DiscordGuild guild = json.ToDiscordObject(); - if (this._discord is DiscordClient dc) - await dc.OnGuildUpdateEventAsync(guild, rawMembers); - return guild; + if (this._discord is DiscordClient dc) + { + await dc.OnGuildCreateEventAsync(guild, rawMembers, null!); } - internal async Task> GetGuildBansAsync(ulong guild_id, int? limit, ulong? before, ulong? after) - { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + return guild; + } - var queryParams = new Dictionary(); - if (limit != null) - queryParams["limit"] = limit.ToString(); - if (before != null) - queryParams["before"] = before.ToString(); - if (after != null) - queryParams["after"] = after.ToString(); + internal async ValueTask DeleteGuildAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}", + Url = $"{Endpoints.GUILDS}/{guildId}", + Method = HttpMethod.Delete + }; - var url = Utilities.GetApiUriFor(path, BuildQueryString(queryParams)); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + _ = await this._rest.ExecuteRequestAsync(request); + } - var bans_raw = JsonConvert.DeserializeObject>(res.Response).Select(xb => - { - if (!this._discord.TryGetCachedUserInternal(xb.RawUser.Id, out var usr)) + internal async ValueTask ModifyGuildAsync + ( + ulong guildId, + Optional name = default, + Optional region = default, + Optional verificationLevel = default, + Optional defaultMessageNotifications = default, + Optional mfaLevel = default, + Optional explicitContentFilter = default, + Optional afkChannelId = default, + Optional afkTimeout = default, + Optional iconb64 = default, + Optional ownerId = default, + Optional splashb64 = default, + Optional systemChannelId = default, + Optional banner = default, + Optional description = default, + Optional discoverySplash = default, + Optional> features = default, + Optional preferredLocale = default, + Optional publicUpdatesChannelId = default, + Optional rulesChannelId = default, + Optional systemChannelFlags = default, + string? reason = null + ) + { + RestGuildModifyPayload payload = new() + { + Name = name, + RegionId = region, + VerificationLevel = verificationLevel, + DefaultMessageNotifications = defaultMessageNotifications, + MfaLevel = mfaLevel, + ExplicitContentFilter = explicitContentFilter, + AfkChannelId = afkChannelId, + AfkTimeout = afkTimeout, + IconBase64 = iconb64, + SplashBase64 = splashb64, + OwnerId = ownerId, + SystemChannelId = systemChannelId, + Banner = banner, + Description = description, + DiscoverySplash = discoverySplash, + Features = features, + PreferredLocale = preferredLocale, + PublicUpdatesChannelId = publicUpdatesChannelId, + RulesChannelId = rulesChannelId, + SystemChannelFlags = systemChannelFlags + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}", + Url = $"{Endpoints.GUILDS}/{guildId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary { - usr = new DiscordUser(xb.RawUser) { Discord = this._discord }; - usr = this._discord.UpdateUserCache(usr); + [REASON_HEADER_NAME] = reason } + }; - xb.User = usr; - return xb; - }); - var bans = new ReadOnlyCollection(new List(bans_raw)); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - return bans; + JObject json = JObject.Parse(res.Response!); + JArray rawMembers = (JArray)json["members"]!; + DiscordGuild guild = json.ToDiscordObject(); + foreach (DiscordRole r in guild._roles.Values) + { + r._guild_id = guild.Id; } - internal Task CreateGuildBanAsync(ulong guild_id, ulong user_id, int delete_message_days, string reason) + if (this._discord is DiscordClient dc) { - if (delete_message_days < 0 || delete_message_days > 7) - throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(delete_message_days)); - - var urlparams = new Dictionary - { - ["delete_message_days"] = delete_message_days.ToString(CultureInfo.InvariantCulture) - }; + await dc.OnGuildUpdateEventAsync(guild, rawMembers!); + } - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + return guild; + } - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path); + internal async ValueTask> GetGuildBansAsync + ( + ulong guildId, + int? limit = null, + ulong? before = null, + ulong? after = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}"); - var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams)); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, headers); + if(limit is not null) + { + builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); } - internal Task RemoveGuildBanAsync(ulong guild_id, ulong user_id, string reason) + if(before is not null) { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path); + builder.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); + } - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + if(after is not null) + { + builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); } - internal Task LeaveGuildAsync(ulong guild_id) + RestRequest request = new() { - var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}", + Url = builder.Build(), + Method = HttpMethod.Get + }; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); - } + RestResponse res = await this._rest.ExecuteRequestAsync(request); - internal async Task AddGuildMemberAsync(ulong guild_id, ulong user_id, string access_token, string nick, IEnumerable roles, bool muted, bool deafened) + IEnumerable bansRaw = JsonConvert.DeserializeObject>(res.Response!)! + .Select(xb => { - var pld = new RestGuildMemberAddPayload + if (!this._discord!.TryGetCachedUserInternal(xb.RawUser.Id, out DiscordUser? user)) { - AccessToken = access_token, - Nickname = nick ?? "", - Roles = roles ?? new List(), - Deaf = deafened, - Mute = muted - }; + user = new DiscordUser(xb.RawUser) { Discord = this._discord }; + user = this._discord.UpdateUserCache(user); + } - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path); + xb.User = user; + return xb; + }); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); + ReadOnlyCollection bans = new(new List(bansRaw)); - var tm = JsonConvert.DeserializeObject(res.Response); + return bans; + } - return new DiscordMember(tm) { Discord = this._discord, _guild_id = guild_id }; + internal async ValueTask CreateGuildBanAsync + ( + ulong guildId, + ulong userId, + int deleteMessageDays, + string? reason = null + ) + { + if (deleteMessageDays < 0 || deleteMessageDays > 7) + { + throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(deleteMessageDays)); } - internal async Task> ListGuildMembersAsync(ulong guild_id, int? limit, ulong? after) + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}"); + + builder.AddParameter("delete_message_days", deleteMessageDays.ToString(CultureInfo.InvariantCulture)); + + RestRequest request = new() { - var urlparams = new Dictionary(); - if (limit != null && limit > 0) - urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); - if (after != null) - urlparams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", + Url = builder.Build(), + Method = HttpMethod.Put, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + _ = await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : ""); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + internal async ValueTask RemoveGuildBanAsync + ( + ulong guildId, + ulong userId, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}", + Method = HttpMethod.Delete, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - var members_raw = JsonConvert.DeserializeObject>(res.Response); - return new ReadOnlyCollection(members_raw); - } + _ = await this._rest.ExecuteRequestAsync(request); + } - internal Task AddGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) + internal async ValueTask LeaveGuildAsync + ( + ulong guildId + ) + { + RestRequest request = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}/{guildId}", + Url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}/{guildId}", + Method = HttpMethod.Delete + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id, role_id }, out var path); + _ = await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, headers); - } + internal async ValueTask AddGuildMemberAsync + ( + ulong guildId, + ulong userId, + string accessToken, + bool? muted = null, + bool? deafened = null, + string? nick = null, + IEnumerable? roles = null + ) + { + RestGuildMemberAddPayload payload = new() + { + AccessToken = accessToken, + Nickname = nick ?? "", + Roles = roles ?? new List(), + Deaf = deafened ?? false, + Mute = muted ?? false + }; - internal Task RemoveGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) + RestRequest request = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:{userId}", + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(payload) + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id, role_id }, out var path); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); - } + TransportMember transport = JsonConvert.DeserializeObject(res.Response!)!; - internal Task ModifyGuildChannelPositionAsync(ulong guild_id, IEnumerable pld, string reason) - { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + return new DiscordMember(transport) { Discord = this._discord!, _guild_id = guildId }; + } - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); + internal async ValueTask> ListGuildMembersAsync + ( + ulong guildId, + int? limit = null, + ulong? after = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}"); - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + if (limit is not null && limit > 0) + { + builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); } - internal Task ModifyGuildRolePositionAsync(ulong guild_id, IEnumerable pld, string reason) + if (after is not null) { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); } - internal async Task ModifyGuildRolePositionsAsync(ulong guild_id, IEnumerable newRolePositions, string reason) + RestRequest request = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(newRolePositions)); - - var ret = JsonConvert.DeserializeObject(res.Response); - foreach (var r in ret) - { - r.Discord = this._discord; - r._guild_id = guild_id; - } + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}", + Url = builder.Build(), + Method = HttpMethod.Get + }; - return ret; - } + RestResponse res = await this._rest.ExecuteRequestAsync(request); - internal async Task GetAuditLogsAsync(ulong guild_id, int limit, ulong? after, ulong? before, ulong? responsible, int? action_type) - { - var urlparams = new Dictionary - { - ["limit"] = limit.ToString(CultureInfo.InvariantCulture) - }; - if (after != null) - urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture); - if (before != null) - urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture); - if (responsible != null) - urlparams["user_id"] = responsible?.ToString(CultureInfo.InvariantCulture); - if (action_type != null) - urlparams["action_type"] = action_type?.ToString(CultureInfo.InvariantCulture); + List rawMembers = JsonConvert.DeserializeObject>(res.Response!)!; + return new ReadOnlyCollection(rawMembers); + } - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUDIT_LOGS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + internal async ValueTask AddGuildMemberRoleAsync + ( + ulong guildId, + ulong userId, + ulong roleId, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id/{Endpoints.ROLES}/:role_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}/{Endpoints.ROLES}/{roleId}", + Method = HttpMethod.Put, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : ""); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + _ = await this._rest.ExecuteRequestAsync(request); + } - var audit_log_data_raw = JsonConvert.DeserializeObject(res.Response); + internal async ValueTask RemoveGuildMemberRoleAsync + ( + ulong guildId, + ulong userId, + ulong roleId, + string reason + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id/{Endpoints.ROLES}/:role_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}/{Endpoints.ROLES}/{roleId}", + Method = HttpMethod.Delete, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - return audit_log_data_raw; - } + _ = await this._rest.ExecuteRequestAsync(request); + } - internal async Task GetGuildVanityUrlAsync(ulong guild_id) - { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VANITY_URL}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + internal async ValueTask ModifyGuildChannelPositionAsync + ( + ulong guildId, + IEnumerable payload, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + await this._rest.ExecuteRequestAsync(request); + } - var invite = JsonConvert.DeserializeObject(res.Response); + // TODO: should probably return an IReadOnlyList here, unsure as to the extent of the breaking change + internal async ValueTask ModifyGuildRolePositionsAsync + ( + ulong guildId, + IEnumerable newRolePositions, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(newRolePositions), + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - return invite; - } + RestResponse res = await this._rest.ExecuteRequestAsync(request); - internal async Task GetGuildWidgetAsync(ulong guild_id) + DiscordRole[] ret = JsonConvert.DeserializeObject(res.Response!)!; + foreach (DiscordRole role in ret) { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET_JSON}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + role.Discord = this._discord!; + role._guild_id = guildId; + } - var json = JObject.Parse(res.Response); - var rawChannels = (JArray)json["channels"]; + return ret; + } - var ret = json.ToDiscordObject(); - ret.Discord = this._discord; - ret.Guild = this._discord.Guilds[guild_id]; + internal async ValueTask GetAuditLogsAsync + ( + ulong guildId, + int limit, + ulong? after = null, + ulong? before = null, + ulong? userId = null, + AuditLogActionType? actionType = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUDIT_LOGS}"); - ret.Channels = ret.Guild == null - ? rawChannels.Select(r => new DiscordChannel - { - Id = (ulong)r["id"], - Name = r["name"].ToString(), - Position = (int)r["position"] - }).ToList() - : rawChannels.Select(r => - { - var c = ret.Guild.GetChannel((ulong)r["id"]); - c.Position = (int)r["position"]; - return c; - }).ToList(); + builder.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - return ret; + if(after is not null) + { + builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); } - internal async Task GetGuildWidgetSettingsAsync(ulong guild_id) + if(before is not null) { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + builder.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Guild = this._discord.Guilds[guild_id]; + if(userId is not null) + { + builder.AddParameter("user_id", userId.Value.ToString(CultureInfo.InvariantCulture)); + } - return ret; + if(actionType is not null) + { + builder.AddParameter("action_type", ((int)actionType.Value).ToString(CultureInfo.InvariantCulture)); } - internal async Task ModifyGuildWidgetSettingsAsync(ulong guild_id, bool? isEnabled, ulong? channelId, string reason) + RestRequest request = new() { - var pld = new RestGuildWidgetSettingsPayload - { - Enabled = isEnabled, - ChannelId = channelId - }; + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUDIT_LOGS}", + Url = builder.Build(), + Method = HttpMethod.Get + }; - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); + return JsonConvert.DeserializeObject(res.Response!)!; + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + internal async ValueTask GetGuildVanityUrlAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VANITY_URL}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VANITY_URL}", + Method = HttpMethod.Get + }; - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Guild = this._discord.Guilds[guild_id]; + RestResponse res = await this._rest.ExecuteRequestAsync(request); - return ret; - } + return JsonConvert.DeserializeObject(res.Response!)!; + } - internal async Task> GetGuildTemplatesAsync(ulong guild_id) + internal async ValueTask GetGuildWidgetAsync + ( + ulong guildId + ) + { + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET_JSON}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET_JSON}", + Method = HttpMethod.Get + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var templates_raw = JsonConvert.DeserializeObject>(res.Response); + // TODO: this should really be cleaned up + JObject json = JObject.Parse(res.Response!); + JArray rawChannels = (JArray)json["channels"]!; - return new ReadOnlyCollection(new List(templates_raw)); - } + DiscordWidget ret = json.ToDiscordObject(); + ret.Discord = this._discord!; + ret.Guild = this._discord!.Guilds[guildId]; - internal async Task CreateGuildTemplateAsync(ulong guild_id, string name, string description) - { - var pld = new RestGuildTemplateCreateOrModifyPayload + ret.Channels = ret.Guild is null + ? rawChannels.Select(r => new DiscordChannel { - Name = name, - Description = description - }; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); - - var ret = JsonConvert.DeserializeObject(res.Response); + Id = (ulong)r["id"]!, + Name = r["name"]!.ToString(), + Position = (int)r["position"]! + }).ToList() + : rawChannels.Select(r => + { + DiscordChannel c = ret.Guild.GetChannel((ulong)r["id"]!); + c.Position = (int)r["position"]!; + return c; + }).ToList(); - return ret; - } + return ret; + } - internal async Task SyncGuildTemplateAsync(ulong guild_id, string template_code) + internal async ValueTask GetGuildWidgetSettingsAsync + ( + ulong guildId + ) + { + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, template_code }, out var path); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", + Method = HttpMethod.Get + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var template_raw = JsonConvert.DeserializeObject(res.Response); + DiscordWidgetSettings ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Guild = this._discord!.Guilds[guildId]; - return template_raw; - } + return ret; + } - internal async Task ModifyGuildTemplateAsync(ulong guild_id, string template_code, string name, string description) + internal async ValueTask ModifyGuildWidgetSettingsAsync + ( + ulong guildId, + bool? isEnabled = null, + ulong? channelId = null, + string? reason = null + ) + { + RestGuildWidgetSettingsPayload payload = new() { - var pld = new RestGuildTemplateCreateOrModifyPayload - { - Name = name, - Description = description - }; + Enabled = isEnabled, + ChannelId = channelId + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, template_code }, out var path); + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = reason is null + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var template_raw = JsonConvert.DeserializeObject(res.Response); + DiscordWidgetSettings ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Guild = this._discord!.Guilds[guildId]; - return template_raw; - } + return ret; + } - internal async Task DeleteGuildTemplateAsync(ulong guild_id, string template_code) + internal async ValueTask> GetGuildTemplatesAsync + ( + ulong guildId + ) + { + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, template_code }, out var path); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", + Method = HttpMethod.Get + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var template_raw = JsonConvert.DeserializeObject(res.Response); + IEnumerable templates = + JsonConvert.DeserializeObject>(res.Response!)!; - return template_raw; - } + return new ReadOnlyCollection(new List(templates)); + } - internal async Task GetGuildMembershipScreeningFormAsync(ulong guild_id) + internal async ValueTask CreateGuildTemplateAsync + ( + ulong guildId, + string name, + string description + ) + { + RestGuildTemplateCreateOrModifyPayload payload = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Name = name, + Description = description + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(payload) + }; - var screening_raw = JsonConvert.DeserializeObject(res.Response); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - return screening_raw; - } + return JsonConvert.DeserializeObject(res.Response!)!; + } - internal async Task ModifyGuildMembershipScreeningFormAsync(ulong guild_id, Optional enabled, Optional fields, Optional description) + internal async ValueTask SyncGuildTemplateAsync + ( + ulong guildId, + string templateCode + ) + { + RestRequest request = new() { - var pld = new RestGuildMembershipScreeningFormModifyPayload - { - Enabled = enabled, - Description = description, - Fields = fields - }; + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", + Method = HttpMethod.Put + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); - - var screening_raw = JsonConvert.DeserializeObject(res.Response); + return JsonConvert.DeserializeObject(res.Response!)!; + } - return screening_raw; - } + internal async ValueTask ModifyGuildTemplateAsync + ( + ulong guildId, + string templateCode, + string? name = null, + string? description = null + ) + { + RestGuildTemplateCreateOrModifyPayload payload = new() + { + Name = name, + Description = description + }; - internal async Task GetGuildWelcomeScreenAsync(ulong guild_id) + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload) + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var ret = JsonConvert.DeserializeObject(res.Response); - return ret; - } + return JsonConvert.DeserializeObject(res.Response!)!; + } - internal async Task ModifyGuildWelcomeScreenAsync(ulong guild_id, Optional enabled, Optional> welcomeChannels, Optional description, string reason) + internal async ValueTask DeleteGuildTemplateAsync + ( + ulong guildId, + string templateCode + ) + { + RestRequest request = new() { - var pld = new RestGuildWelcomeScreenModifyPayload - { - Enabled = enabled, - WelcomeChannels = welcomeChannels, - Description = description - }; - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", + Method = HttpMethod.Delete + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); - - var ret = JsonConvert.DeserializeObject(res.Response); - return ret; - } + return JsonConvert.DeserializeObject(res.Response!)!; + } - internal async Task UpdateCurrentUserVoiceStateAsync(ulong guild_id, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp) + internal async ValueTask GetGuildMembershipScreeningFormAsync + ( + ulong guildId + ) + { + RestRequest request = new() { - var pld = new RestGuildUpdateCurrentUserVoiceStatePayload - { - ChannelId = channelId, - Suppress = suppress, - RequestToSpeakTimestamp = requestToSpeakTimestamp - }; + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", + Method = HttpMethod.Get + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/@me"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); - } + return JsonConvert.DeserializeObject(res.Response!)!; + } - internal async Task UpdateUserVoiceStateAsync(ulong guild_id, ulong user_id, ulong channelId, bool? suppress) + internal async ValueTask ModifyGuildMembershipScreeningFormAsync + ( + ulong guildId, + Optional enabled = default, + Optional fields = default, + Optional description = default + ) + { + RestGuildMembershipScreeningFormModifyPayload payload = new() { - var pld = new RestGuildUpdateUserVoiceStatePayload - { - ChannelId = channelId, - Suppress = suppress - }; + Enabled = enabled, + Description = description, + Fields = fields + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload) + }; - var url = Utilities.GetApiUriFor(path); - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); - } - #endregion + RestResponse res = await this._rest.ExecuteRequestAsync(request); - #region Stickers + return JsonConvert.DeserializeObject(res.Response!)!; + } - internal async Task GetGuildStickerAsync(ulong guild_id, ulong sticker_id) + internal async ValueTask GetGuildWelcomeScreenAsync + ( + ulong guildId + ) + { + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, sticker_id}, out var path); - var url = Utilities.GetApiUriFor(path); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", + Method = HttpMethod.Get + }; - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var json = JObject.Parse(res.Response); - var ret = json.ToDiscordObject(); - - if (json["user"] is JObject jusr) // Null = Missing stickers perm // - { - var tsr = jusr.ToDiscordObject(); - var usr = new DiscordUser(tsr) {Discord = this._discord}; - ret.User = usr; - } + RestResponse res = await this._rest.ExecuteRequestAsync(request); - ret.Discord = this._discord; - return ret; - } + return JsonConvert.DeserializeObject(res.Response!)!; + } - internal async Task GetStickerAsync(ulong sticker_id) - { - var route = $"{Endpoints.STICKERS}/:sticker_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new {sticker_id}, out var path); - var url = Utilities.GetApiUriFor(path); + internal async ValueTask ModifyGuildWelcomeScreenAsync + ( + ulong guildId, + Optional enabled = default, + Optional> welcomeChannels = default, + Optional description = default, + string? reason = null + ) + { + RestGuildWelcomeScreenModifyPayload payload = new() + { + Enabled = enabled, + WelcomeChannels = welcomeChannels, + Description = description + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = reason is null + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var json = JObject.Parse(res.Response); - var ret = json.ToDiscordObject(); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - if (json["user"] is JObject jusr) // Null = Missing stickers perm // - { - var tsr = jusr.ToDiscordObject(); - var usr = new DiscordUser(tsr) {Discord = this._discord}; - ret.User = usr; - } - - ret.Discord = this._discord; - return ret; - } + return JsonConvert.DeserializeObject(res.Response!)!; + } - internal async Task> GetStickerPacksAsync() + internal async ValueTask UpdateCurrentUserVoiceStateAsync + ( + ulong guildId, + ulong channelId, + bool? suppress = null, + DateTimeOffset? requestToSpeakTimestamp = null + ) + { + RestGuildUpdateCurrentUserVoiceStatePayload payload = new() { - var route = $"{Endpoints.STICKERPACKS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); + ChannelId = channelId, + Suppress = suppress, + RequestToSpeakTimestamp = requestToSpeakTimestamp + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/@me", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/@me", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload) + }; - var json = JObject.Parse(res.Response)["sticker_packs"] as JArray; - var ret = json.ToDiscordObject(); + _ = await this._rest.ExecuteRequestAsync(request); + } - return ret; - } + internal async ValueTask UpdateUserVoiceStateAsync + ( + ulong guildId, + ulong userId, + ulong channelId, + bool? suppress = null + ) + { + RestGuildUpdateUserVoiceStatePayload payload = new() + { + ChannelId = channelId, + Suppress = suppress + }; - internal async Task> GetGuildStickersAsync(ulong guild_id) + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new {guild_id}, out var path); - var url = Utilities.GetApiUriFor(path); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{userId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload) + }; - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var json = JArray.Parse(res.Response); - var ret = json.ToDiscordObject(); + _ = await this._rest.ExecuteRequestAsync(request); + } + #endregion + #region Stickers - for (var i = 0; i < ret.Length; i++) - { - var stkr = ret[i]; - stkr.Discord = this._discord; + internal async ValueTask GetGuildStickerAsync + ( + ulong guildId, + ulong stickerId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", + Method = HttpMethod.Get + }; - if (json[i]["user"] is JObject jusr) // Null = Missing stickers perm // - { - var tsr = jusr.ToDiscordObject(); - var usr = new DiscordUser(tsr) {Discord = this._discord}; - stkr.User = usr; // The sticker would've already populated, but this is just to ensure everything is up to date // - } - } + RestResponse res = await this._rest.ExecuteRequestAsync(request); + JObject json = JObject.Parse(res.Response!); - return ret; - } + DiscordMessageSticker ret = json.ToDiscordObject(); - internal async Task CreateGuildStickerAsync(ulong guild_id, string name, string description, string tags, DiscordMessageFile file, string reason) + if (json["user"] is JObject jusr) // Null = Missing stickers perm // { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new {guild_id}, out var path); - var url = Utilities.GetApiUriFor(path); - - if (!string.IsNullOrEmpty(reason)) - headers[REASON_HEADER_NAME] = reason; + TransportUser tsr = jusr.ToDiscordObject(); + DiscordUser usr = new(tsr) { Discord = this._discord! }; + ret.User = usr; + } - var values = new Dictionary - { - ["name"] = name, - ["description"] = description, - ["tags"] = tags, - }; + ret.Discord = this._discord!; + return ret; + } - var res = await this.DoMultipartAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, values, new[] {file}, removeFileCount: true); - var json = JObject.Parse(res.Response); - var ret = json.ToDiscordObject(); + internal async ValueTask GetStickerAsync + ( + ulong stickerId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.STICKERS}/:sticker_id", + Url = $"{Endpoints.STICKERS}/{stickerId}", + Method = HttpMethod.Get + }; - if (json["user"] is JObject jusr) // Null = Missing stickers perm // - { - var tsr = jusr.ToDiscordObject(); - var usr = new DiscordUser(tsr) {Discord = this._discord}; - ret.User = usr; - } + RestResponse res = await this._rest.ExecuteRequestAsync(request); + JObject json = JObject.Parse(res.Response!); - ret.Discord = this._discord; + DiscordMessageSticker ret = json.ToDiscordObject(); - return ret; + if (json["user"] is JObject jusr) // Null = Missing stickers perm // + { + TransportUser tsr = jusr.ToDiscordObject(); + DiscordUser usr = new(tsr) { Discord = this._discord! }; + ret.User = usr; } - internal async Task ModifyStickerAsync(ulong guild_id, ulong sticker_id, Optional name, Optional description, Optional tags, string reason) - { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + ret.Discord = this._discord!; + return ret; + } - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id, sticker_id}, out var path); - var url = Utilities.GetApiUriFor(path); + internal async ValueTask> GetStickerPacksAsync() + { + RestRequest request = new() + { + Route = $"{Endpoints.STICKERPACKS}", + Url = $"{Endpoints.STICKERPACKS}", + Method = HttpMethod.Get + }; - var pld = new RestStickerModifyPayload() - { - Name = name, - Description = description, - Tags = tags - }; + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers: headers, payload: DiscordJson.SerializeObject(pld)); - var ret = JObject.Parse(res.Response).ToDiscordObject(); - ret.Discord = this._discord; + JArray json = (JArray)JObject.Parse(res.Response!)["sticker_packs"]!; + DiscordMessageStickerPack[] ret = json.ToDiscordObject(); - return ret; - } + return ret; + } - internal async Task DeleteStickerAsync(ulong guild_id, ulong sticker_id, string reason) + internal async ValueTask> GetGuildStickersAsync + ( + ulong guildId + ) + { + RestRequest request = new() { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", + Method = HttpMethod.Get + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id, sticker_id}, out var path); - var url = Utilities.GetApiUriFor(path); - - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers: headers); - } + RestResponse res = await this._rest.ExecuteRequestAsync(request); + JArray json = JArray.Parse(res.Response!); - #endregion + DiscordMessageSticker[] ret = json.ToDiscordObject(); - #region Channel - internal async Task CreateGuildChannelAsync - ( - ulong guild_id, - string name, - ChannelType type, - ulong? parent, - Optional topic, - int? bitrate, - int? user_limit, - IEnumerable overwrites, - bool? nsfw, - Optional perUserRateLimit, - VideoQualityMode? qualityMode, - int? position, - string reason, - AutoArchiveDuration? defaultAutoArchiveDuration, - DefaultReaction? defaultReactionEmoji, - IEnumerable forumTags, - DefaultSortOrder? defaultSortOrder - - ) + for (int i = 0; i < ret.Length; i++) { - var restoverwrites = new List(); - if (overwrites != null) - foreach (var ow in overwrites) - restoverwrites.Add(ow.Build()); + DiscordMessageSticker sticker = ret[i]; + sticker.Discord = this._discord!; - var pld = new RestChannelCreatePayload + if (json[i]["user"] is JObject jusr) // Null = Missing stickers perm // { - Name = name, - Type = type, - Parent = parent, - Topic = topic, - Bitrate = bitrate, - UserLimit = user_limit, - PermissionOverwrites = restoverwrites, - Nsfw = nsfw, - PerUserRateLimit = perUserRateLimit, - QualityMode = qualityMode, - Position = position, - DefaultAutoArchiveDuration = defaultAutoArchiveDuration, - DefaultReaction = defaultReactionEmoji, - AvailableTags = forumTags, - DefaultSortOrder = defaultSortOrder - }; - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); + TransportUser transportUser = jusr.ToDiscordObject(); + DiscordUser user = new(transportUser) + { + Discord = this._discord! + }; - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - foreach (var xo in ret._permissionOverwrites) - { - xo.Discord = this._discord; - xo._channel_id = ret.Id; + // The sticker would've already populated, but this is just to ensure everything is up to date + sticker.User = user; } - - return ret; } - internal Task ModifyChannelAsync - ( - ulong channel_id, - string name, - int? position, - Optional topic, - bool? nsfw, - Optional parent, - int? bitrate, - int? user_limit, - Optional perUserRateLimit, - Optional rtcRegion, - VideoQualityMode? qualityMode, - Optional type, - IEnumerable permissionOverwrites, - string reason, - Optional flags, - IEnumerable? availableTags, - Optional defaultAutoArchiveDuration, - Optional defaultReactionEmoji, - Optional defaultPerUserRatelimit, - Optional defaultSortOrder, - Optional defaultForumLayout - ) - { - List restoverwrites = null; - if (permissionOverwrites != null) - { - restoverwrites = new List(); - foreach (var ow in permissionOverwrites) - restoverwrites.Add(ow.Build()); - } + return ret; + } - var pld = new RestChannelModifyPayload + internal async ValueTask CreateGuildStickerAsync + ( + ulong guildId, + string name, + string description, + string tags, + DiscordMessageFile file, + string? reason = null + ) + { + MultipartRestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", + Method = HttpMethod.Post, + Headers = reason is null + ? null + : new Dictionary() + { + [REASON_HEADER_NAME] = reason + }, + Files = new DiscordMessageFile[] { - Name = name, - Position = position, - Topic = topic, - Nsfw = nsfw, - Parent = parent, - Bitrate = bitrate, - UserLimit = user_limit, - PerUserRateLimit = perUserRateLimit, - RtcRegion = rtcRegion, - QualityMode = qualityMode, - Type = type, - PermissionOverwrites = restoverwrites, - Flags = flags, - AvailableTags = availableTags, - DefaultAutoArchiveDuration = defaultAutoArchiveDuration, - DefaultReaction = defaultReactionEmoji, - DefaultForumLayout = defaultForumLayout, - DefaultSortOrder = defaultSortOrder - }; - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.CHANNELS}/:channel_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); - } - - internal Task ModifyThreadChannelAsync - ( - ulong channel_id, - string name, - int? position, - Optional topic, - bool? nsfw, - Optional parent, - int? bitrate, - int? user_limit, - Optional perUserRateLimit, - Optional rtcRegion, - VideoQualityMode? qualityMode, - Optional type, - IEnumerable permissionOverwrites, - bool? isArchived, - AutoArchiveDuration? autoArchiveDuration, - bool? locked, - string reason, - IEnumerable applied_tags - ) - { - List restoverwrites = null; - if (permissionOverwrites != null) + file + }, + Values = new Dictionary() { - restoverwrites = new List(); - foreach (var ow in permissionOverwrites) - restoverwrites.Add(ow.Build()); + ["name"] = name, + ["description"] = description, + ["tags"] = tags, } + }; - var pld = new RestThreadChannelModifyPayload - { - Name = name, - Position = position, - Topic = topic, - Nsfw = nsfw, - Parent = parent, - Bitrate = bitrate, - UserLimit = user_limit, - PerUserRateLimit = perUserRateLimit, - RtcRegion = rtcRegion, - QualityMode = qualityMode, - Type = type, - PermissionOverwrites = restoverwrites, - IsArchived = isArchived, - ArchiveDuration = autoArchiveDuration, - Locked = locked, - AppliedTags = applied_tags - }; - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers.Add(REASON_HEADER_NAME, reason); + RestResponse res = await this._rest.ExecuteRequestAsync(request); + JObject json = JObject.Parse(res.Response!); - var route = $"{Endpoints.CHANNELS}/:channel_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); - } + DiscordMessageSticker ret = json.ToDiscordObject(); - internal async Task> GetScheduledGuildEventsAsync(ulong guild_id, bool with_user_counts = false) + if (json["user"] is JObject rawUser) // Null = Missing stickers perm // { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EVENTS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + TransportUser transportUser = rawUser.ToDiscordObject(); - var query = new Dictionary() { { "with_user_count", with_user_counts.ToString() } }; - - var url = Utilities.GetApiUriFor(path, BuildQueryString(query)); - - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route, new Dictionary(), string.Empty); - - var ret = JsonConvert.DeserializeObject(res.Response)!.ToList(); - - foreach (var xe in ret) - { - xe.Discord = this._discord; - - if (xe.Creator != null) - xe.Creator.Discord = this._discord; - } + DiscordUser user = new(transportUser) + { + Discord = this._discord! + }; - return ret.AsReadOnly(); + ret.User = user; } - internal async Task CreateScheduledGuildEventAsync(ulong guild_id, string name, string description, ulong? channel_id, DateTimeOffset start_time, DateTimeOffset? end_time, ScheduledGuildEventType type, ScheduledGuildEventPrivacyLevel privacy_level, DiscordScheduledGuildEventMetadata metadata, string reason = null) - { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EVENTS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); + ret.Discord = this._discord!; - var headers = Utilities.GetBaseHeaders(); - - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var url = Utilities.GetApiUriFor(path); - - var pld = new RestScheduledGuildEventCreatePayload - { - Name = name, - Description = description, - ChannelId = channel_id, - StartTime = start_time, - EndTime = end_time, - Type = type, - PrivacyLevel = privacy_level, - Metadata = metadata - }; + return ret; + } - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); + internal async ValueTask ModifyStickerAsync + ( + ulong guildId, + ulong stickerId, + Optional name = default, + Optional description = default, + Optional tags = default, + string? reason = null + ) + { + RestStickerModifyPayload payload = new() + { + Name = name, + Description = description, + Tags = tags + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = reason is null + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; - var ret = JsonConvert.DeserializeObject(res.Response); + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordMessageSticker ret = JObject.Parse(res.Response!).ToDiscordObject(); + ret.Discord = this._discord!; - ret.Discord = this._discord; + return ret; + } - if (ret.Creator != null) - ret.Creator.Discord = this._discord; + internal async ValueTask DeleteStickerAsync + ( + ulong guildId, + ulong stickerId, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", + Method = HttpMethod.Delete, + Headers = reason is null + ? null + : new Dictionary() + { + [REASON_HEADER_NAME] = reason + } + }; - return ret; - } + await this._rest.ExecuteRequestAsync(request); + } - internal async Task DeleteScheduledGuildEventAsync(ulong guild_id, ulong guild_scheduled_event_id) + #endregion + + #region Channel + internal async ValueTask CreateGuildChannelAsync + ( + ulong guildId, + string name, + ChannelType type, + ulong? parent, + Optional topic, + int? bitrate, + int? userLimit, + IEnumerable overwrites, + bool? nsfw, + Optional perUserRateLimit, + VideoQualityMode? qualityMode, + int? position, + string reason, + AutoArchiveDuration? defaultAutoArchiveDuration, + DefaultReaction? defaultReactionEmoji, + IEnumerable forumTags, + DefaultSortOrder? defaultSortOrder + + ) + { + List restOverwrites = new(); + if (overwrites != null) { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EVENTS}/:guild_scheduled_event_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, guild_scheduled_event_id }, out var path); - - var headers = Utilities.GetBaseHeaders(); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers, null); + foreach (DiscordOverwriteBuilder ow in overwrites) + { + restOverwrites.Add(ow.Build()); + } } - internal async Task> GetScheduledGuildEventUsersAsync(ulong guild_id, ulong guild_scheduled_event_id, bool with_members = false, int limit = 1, ulong? before = null, ulong? after = null) + RestChannelCreatePayload pld = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EVENTS}/:guild_scheduled_event_id{Endpoints.USERS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, guild_scheduled_event_id }, out var path); - - var query = new Dictionary() { { "with_members", with_members.ToString() } }; - - if (limit > 0) - query.Add("limit", limit.ToString(CultureInfo.InvariantCulture)); - - if (before != null) - query.Add("before", before.Value.ToString(CultureInfo.InvariantCulture)); - - if (after != null) - query.Add("after", after.Value.ToString(CultureInfo.InvariantCulture)); - - var url = Utilities.GetApiUriFor(path, BuildQueryString(query)); + Name = name, + Type = type, + Parent = parent, + Topic = topic, + Bitrate = bitrate, + UserLimit = userLimit, + PermissionOverwrites = restOverwrites, + Nsfw = nsfw, + PerUserRateLimit = perUserRateLimit, + QualityMode = qualityMode, + Position = position, + DefaultAutoArchiveDuration = defaultAutoArchiveDuration, + DefaultReaction = defaultReactionEmoji, + AvailableTags = forumTags, + DefaultSortOrder = defaultSortOrder + }; - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route, new Dictionary(), string.Empty); - - var jto = JToken.Parse(res.Response); - - return (jto as JArray ?? jto["users"] as JArray) - .Select(j => (DiscordUser) - j - .SelectToken("member")? - .ToDiscordObject() ?? - j - .SelectToken("user") - .ToDiscordObject()) - .ToArray(); + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task GetScheduledGuildEventAsync(ulong guild_id, ulong guild_scheduled_event_id) + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EVENTS}/:guild_scheduled_event_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, guild_scheduled_event_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route, Utilities.GetBaseHeaders(), string.Empty); + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", + Url = $"${Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var ret = JsonConvert.DeserializeObject(res.Response); - - ret.Discord = this._discord; - - if (ret.Creator != null) - ret.Creator.Discord = this._discord; - - return ret; - } + DiscordChannel ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - internal async Task ModifyScheduledGuildEventAsync(ulong guild_id, ulong guild_scheduled_event_id, Optional name, Optional description, Optional channel_id, Optional start_time, Optional end_time, Optional type, Optional privacy_level, Optional metadata, Optional status, string reason = null) + foreach (DiscordOverwrite xo in ret._permissionOverwrites) { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EVENTS}/:guild_scheduled_event_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, guild_scheduled_event_id }, out var path); - - var headers = Utilities.GetBaseHeaders(); + xo.Discord = this._discord!; + xo._channel_id = ret.Id; + } - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + return ret; + } - var url = Utilities.GetApiUriFor(path); - var pld = new RestScheduledGuildEventModifyPayload + internal async ValueTask ModifyChannelAsync + ( + ulong channelId, + string name, + int? position = null, + Optional topic = default, + bool? nsfw = null, + Optional parent = default, + int? bitrate = null, + int? userLimit = null, + Optional perUserRateLimit = default, + Optional rtcRegion = default, + VideoQualityMode? qualityMode = null, + Optional type = default, + IEnumerable? permissionOverwrites = null, + Optional flags = default, + IEnumerable? availableTags = null, + Optional defaultAutoArchiveDuration = default, + Optional defaultReactionEmoji = default, + Optional defaultPerUserRatelimit = default, + Optional defaultSortOrder = default, + Optional defaultForumLayout = default, + string? reason = null + ) + { + List? restOverwrites = null; + if (permissionOverwrites is not null) + { + restOverwrites = new(); + foreach (DiscordOverwriteBuilder ow in permissionOverwrites) { - Name = name, - Description = description, - ChannelId = channel_id, - StartTime = start_time, - EndTime = end_time, - Type = type, - PrivacyLevel = privacy_level, - Metadata = metadata, - Status = status - }; - - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); - - var ret = JsonConvert.DeserializeObject(res.Response); - - ret.Discord = this._discord; - - if (ret.Creator != null) - ret.Creator.Discord = this._discord; - - return ret; + restOverwrites.Add(ow.Build()); + } } - internal async Task GetChannelAsync(ulong channel_id) - { - var route = $"{Endpoints.CHANNELS}/:channel_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var ret = JsonConvert.DeserializeObject(res.Response); - - if (ret.IsThread) - ret = JsonConvert.DeserializeObject(res.Response); + RestChannelModifyPayload pld = new() + { + Name = name, + Position = position, + Topic = topic, + Nsfw = nsfw, + Parent = parent, + Bitrate = bitrate, + UserLimit = userLimit, + PerUserRateLimit = perUserRateLimit, + RtcRegion = rtcRegion, + QualityMode = qualityMode, + Type = type, + PermissionOverwrites = restOverwrites, + Flags = flags, + AvailableTags = availableTags, + DefaultAutoArchiveDuration = defaultAutoArchiveDuration, + DefaultReaction = defaultReactionEmoji, + DefaultPerUserRateLimit = defaultPerUserRatelimit, + DefaultForumLayout = defaultForumLayout, + DefaultSortOrder = defaultSortOrder + }; + + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestRequest request = new() + { + Route = $"{Endpoints.CHANNELS}/{channelId}", + Url = $"{Endpoints.CHANNELS}/{channelId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - ret.Discord = this._discord; - foreach (var xo in ret._permissionOverwrites) + internal async ValueTask ModifyThreadChannelAsync + ( + ulong channelId, + string name, + int? position = null, + Optional topic = default, + bool? nsfw = null, + Optional parent = default, + int? bitrate = null, + int? userLimit = null, + Optional perUserRateLimit = default, + Optional rtcRegion = default, + VideoQualityMode? qualityMode = null, + Optional type = default, + IEnumerable? permissionOverwrites = null, + bool? isArchived = null, + AutoArchiveDuration? autoArchiveDuration = null, + bool? locked = null, + IEnumerable? appliedTags = null, + string? reason = null + ) + { + List? restOverwrites = null; + if (permissionOverwrites is not null) + { + restOverwrites = new List(); + foreach (DiscordOverwriteBuilder ow in permissionOverwrites) { - xo.Discord = this._discord; - xo._channel_id = ret.Id; + restOverwrites.Add(ow.Build()); } - - return ret; } - internal Task DeleteChannelAsync(ulong channel_id, string reason) - { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.CHANNELS}/:channel_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); - } + RestThreadChannelModifyPayload pld = new() + { + Name = name, + Position = position, + Topic = topic, + Nsfw = nsfw, + Parent = parent, + Bitrate = bitrate, + UserLimit = userLimit, + PerUserRateLimit = perUserRateLimit, + RtcRegion = rtcRegion, + QualityMode = qualityMode, + Type = type, + PermissionOverwrites = restOverwrites, + IsArchived = isArchived, + ArchiveDuration = autoArchiveDuration, + Locked = locked, + AppliedTags = appliedTags + }; + + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers.Add(REASON_HEADER_NAME, reason); + } + + RestRequest request = new() + { + Route = $"{Endpoints.CHANNELS}/{channelId}", + Url = $"{Endpoints.CHANNELS}/{channelId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - internal async Task GetMessageAsync(ulong channel_id, ulong message_id) + internal async ValueTask> GetScheduledGuildEventsAsync + ( + ulong guildId, + bool withUserCounts = false + ) + { + QueryUriBuilder url = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}"); + url.AddParameter("with_user_count", withUserCounts.ToString()); + + RestRequest request = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var ret = this.PrepareMessage(JObject.Parse(res.Response)); - - return ret; - } + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", + Url = url.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - internal async Task CreateMessageAsync(ulong channel_id, string content, IEnumerable embeds, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply, bool suppressNotifications) + DiscordScheduledGuildEvent[] ret = JsonConvert.DeserializeObject(res.Response!)!; + + foreach (DiscordScheduledGuildEvent? scheduledGuildEvent in ret) { - if (content != null && content.Length > 2000) - throw new ArgumentException("Message content length cannot exceed 2000 characters."); + scheduledGuildEvent.Discord = this._discord!; - if (!embeds?.Any() ?? true) + if (scheduledGuildEvent.Creator is not null) { - if (content == null) - throw new ArgumentException("You must specify message content or an embed."); - - if (content.Length == 0) - throw new ArgumentException("Message content must not be empty."); + scheduledGuildEvent.Creator.Discord = this._discord!; } + } - if (embeds != null) - foreach (var embed in embeds) - if (embed.Timestamp != null) - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - - var pld = new RestChannelMessageCreatePayload - { - HasContent = content != null, - Content = content, - IsTTS = false, - HasEmbed = embeds?.Any() ?? false, - Embeds = embeds, - Flags = suppressNotifications ? MessageFlags.SupressNotifications : 0, - }; - - if (replyMessageId != null) - pld.MessageReference = new InternalDiscordMessageReference { MessageId = replyMessageId, FailIfNotExists = failOnInvalidReply }; - - if (replyMessageId != null) - pld.Mentions = new DiscordMentions(Mentions.All, true, mentionReply); - - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + return ret.AsReadOnly(); + } - var ret = this.PrepareMessage(JObject.Parse(res.Response)); + internal async ValueTask CreateScheduledGuildEventAsync + ( + ulong guildId, + string name, + string description, + DateTimeOffset startTime, + ScheduledGuildEventType type, + ScheduledGuildEventPrivacyLevel privacyLevel, + DiscordScheduledGuildEventMetadata? metadata = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + string? reason = null + ) + { + Dictionary headers = new(); - return ret; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task CreateMessageAsync(ulong channel_id, DiscordMessageBuilder builder) + RestScheduledGuildEventCreatePayload pld = new() { - builder.Validate(); + Name = name, + Description = description, + ChannelId = channelId, + StartTime = startTime, + EndTime = endTime, + Type = type, + PrivacyLevel = privacyLevel, + Metadata = metadata + }; - if (builder.Embeds != null) - foreach (var embed in builder.Embeds) - if (embed?.Timestamp != null) - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - - var pld = new RestChannelMessageCreatePayload - { - HasContent = builder.Content != null, - Content = builder.Content, - StickersIds = builder._stickers?.Where(s => s != null).Select(s => s.Id).ToArray(), - IsTTS = builder.IsTTS, - HasEmbed = builder.Embeds != null, - Embeds = builder.Embeds, - Components = builder.Components - }; + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; - if (builder.ReplyId != null) - pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; + RestResponse res = await this._rest.ExecuteRequestAsync(request); - pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.None, builder.Mentions?.Any() ?? false, builder.MentionOnReply); + DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; - if (builder.Files.Count == 0) - { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); + ret.Discord = this._discord!; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + if (ret.Creator is not null) + { + ret.Creator.Discord = this._discord!; + } - var ret = this.PrepareMessage(JObject.Parse(res.Response)); - return ret; - } - else - { - var values = new Dictionary - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; + return ret; + } - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); + internal async ValueTask DeleteScheduledGuildEventAsync + ( + ulong guildId, + ulong guildScheduledEventId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}", + Method = HttpMethod.Delete + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoMultipartAsync(this._discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); + await this._rest.ExecuteRequestAsync(request); + } - var ret = this.PrepareMessage(JObject.Parse(res.Response)); + internal async ValueTask> GetScheduledGuildEventUsersAsync + ( + ulong guildId, + ulong guildScheduledEventId, + bool withMembers = false, + int limit = 1, + ulong? before = null, + ulong? after = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id/{Endpoints.USERS}"; - builder.ResetFileStreamPositions(); + QueryUriBuilder url = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}/{Endpoints.USERS}"); + + url.AddParameter("with_members", withMembers.ToString()); - return ret; - } + if (limit > 0) + { + url.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); } - internal async Task> GetGuildChannelsAsync(ulong guild_id) + if (before != null) { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var channels_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this._discord; return xc; }); - - foreach (var ret in channels_raw) - foreach (var xo in ret._permissionOverwrites) - { - xo.Discord = this._discord; - xo._channel_id = ret.Id; - } - - return new ReadOnlyCollection(new List(channels_raw)); + url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); } - internal async Task> GetChannelMessagesAsync(ulong channel_id, int limit, ulong? before, ulong? after, ulong? around) + if (after != null) { - var urlparams = new Dictionary(); - if (around != null) - urlparams["around"] = around?.ToString(CultureInfo.InvariantCulture); - if (before != null) - urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture); - if (after != null) - urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture); - if (limit > 0) - urlparams["limit"] = limit.ToString(CultureInfo.InvariantCulture); - - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : ""); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var msgs_raw = JArray.Parse(res.Response); - var msgs = new List(); - foreach (var xj in msgs_raw) - msgs.Add(this.PrepareMessage(xj)); - - return new ReadOnlyCollection(new List(msgs)); + url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); } - - internal async Task GetChannelMessageAsync(ulong channel_id, ulong message_id) + + RestRequest request = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path); + Route = route, + Url = url.Build(), + Method = HttpMethod.Get + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var ret = this.PrepareMessage(JObject.Parse(res.Response)); + JToken jto = JToken.Parse(res.Response!); - return ret; - } + return (jto as JArray ?? jto["users"] as JArray)! + .Select + ( + j => (DiscordUser)j.SelectToken("member")?.ToDiscordObject()! + ?? j.SelectToken("user")!.ToDiscordObject() + ) + .ToArray(); + } - internal async Task EditMessageAsync(ulong channel_id, ulong message_id, Optional content, Optional> embeds, Optional> mentions, IReadOnlyList components, IReadOnlyCollection files, MessageFlags? flags, IEnumerable attachments) + internal async ValueTask GetScheduledGuildEventAsync + ( + ulong guildId, + ulong guildScheduledEventId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}"; + + RestRequest request = new() { - if (embeds.HasValue && embeds.Value != null) - foreach (var embed in embeds.Value) - if (embed.Timestamp != null) - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - - var pld = new RestChannelMessageEditPayload - { - HasContent = content.HasValue, - Content = content.HasValue ? (string)content : null, - HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), - Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, - Components = components, - Flags = flags, - Attachments = attachments, - Mentions = mentions.HasValue ? new DiscordMentions(mentions.Value ?? Mentions.None, false, mentions.Value?.OfType().Any() ?? false) : null - }; + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var values = new Dictionary - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; + DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id, message_id }, out var path); + ret.Discord = this._discord!; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoMultipartAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: files); + if (ret.Creator is not null) + { + ret.Creator.Discord = this._discord!; + } - var ret = this.PrepareMessage(JObject.Parse(res.Response)); + return ret; + } - foreach (var file in files.Where(x => x.ResetPositionTo.HasValue)) - { - file.Stream.Position = file.ResetPositionTo.Value; - } + internal async ValueTask ModifyScheduledGuildEventAsync + ( + ulong guildId, + ulong guildScheduledEventId, + Optional name = default, + Optional description = default, + Optional channelId = default, + Optional startTime = default, + Optional endTime = default, + Optional type = default, + Optional privacyLevel = default, + Optional metadata = default, + Optional status = default, + string? reason = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}"; - return ret; + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - - internal Task DeleteMessageAsync(ulong channel_id, ulong message_id, string reason) + + RestScheduledGuildEventModifyPayload pld = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Name = name, + Description = description, + ChannelId = channelId, + StartTime = startTime, + EndTime = endTime, + Type = type, + PrivacyLevel = privacyLevel, + Metadata = metadata, + Status = status + }; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); + DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + ret.Discord = this._discord!; + + if (ret.Creator is not null) + { + ret.Creator.Discord = this._discord!; } - internal Task DeleteMessagesAsync(ulong channel_id, IEnumerable message_ids, string reason) + return ret; + } + + internal async ValueTask GetChannelAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}"; + string url = $"{Endpoints.CHANNELS}/{channelId}"; + + RestRequest request = new() { - var pld = new RestChannelMessageBulkDeletePayload - { - Messages = message_ids - }; + Route = route, + Url = url, + Method = HttpMethod.Get + }; - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}{Endpoints.BULK_DELETE}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); + DiscordChannel ret = JsonConvert.DeserializeObject(res.Response!)!; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); + // this is really weird, we should consider doing this better + if (ret.IsThread) + { + ret = JsonConvert.DeserializeObject(res.Response!)!; } - internal async Task> GetChannelInvitesAsync(ulong channel_id) + ret.Discord = this._discord!; + foreach (DiscordOverwrite xo in ret._permissionOverwrites) { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + xo.Discord = this._discord!; + xo._channel_id = ret.Id; + } - var invites_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this._discord; return xi; }); + return ret; + } - return new ReadOnlyCollection(new List(invites_raw)); + internal async ValueTask DeleteChannelAsync + ( + ulong channelId, + string reason + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task CreateChannelInviteAsync(ulong channel_id, int max_age, int max_uses, bool temporary, bool unique, string reason, InviteTargetType? targetType, ulong? targetUserId, ulong? targetApplicationId) + RestRequest request = new() { - var pld = new RestChannelInviteCreatePayload - { - MaxAge = max_age, - MaxUses = max_uses, - Temporary = temporary, - Unique = unique, - TargetType = targetType, - TargetUserId = targetUserId, - TargetApplicationId = targetApplicationId - }; - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = $"{Endpoints.CHANNELS}/{channelId}", + Url = new($"{Endpoints.CHANNELS}/{channelId}"), + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); + internal async ValueTask GetMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + return ret; + } - return ret; + internal async ValueTask CreateMessageAsync + ( + ulong channelId, + string? content, + IEnumerable? embeds, + ulong? replyMessageId, + bool mentionReply, + bool failOnInvalidReply, + bool suppressNotifications + ) + { + if (content != null && content.Length > 2000) + { + throw new ArgumentException("Message content length cannot exceed 2000 characters."); } - internal Task DeleteChannelPermissionAsync(ulong channel_id, ulong overwrite_id, string reason) + if (!embeds?.Any() ?? true) { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, overwrite_id }, out var path); + if (content == null) + { + throw new ArgumentException("You must specify message content or an embed."); + } - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + if (content.Length == 0) + { + throw new ArgumentException("Message content must not be empty."); + } } - internal Task EditChannelPermissionsAsync(ulong channel_id, ulong overwrite_id, Permissions allow, Permissions deny, string type, string reason) + if (embeds is not null) { - var pld = new RestChannelPermissionEditPayload + foreach (DiscordEmbed embed in embeds) { - Type = type, - Allow = allow & PermissionMethods.FULL_PERMS, - Deny = deny & PermissionMethods.FULL_PERMS - }; + if (embed.Title.Length > 256) + { + throw new ArgumentException("Embed title length must not exceed 256 characters."); + } - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + if (embed.Description?.Length > 4096) + { + throw new ArgumentException("Embed description length must not exceed 4096 characters."); + } - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, overwrite_id }, out var path); + if (embed.Fields?.Count > 25) + { + throw new ArgumentException("Embed field count must not exceed 25."); + } - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)); + if (embed.Fields is not null) + { + foreach (DiscordEmbedField field in embed.Fields) + { + if (field.Name.Length > 256) + { + throw new ArgumentException("Embed field name length must not exceed 256 characters."); + } + + if (field.Value.Length > 1024) + { + throw new ArgumentException("Embed field value length must not exceed 1024 characters."); + } + } + } + + if (embed.Footer?.Text.Length > 2048) + { + throw new ArgumentException("Embed footer text length must not exceed 2048 characters."); + } + + if (embed.Author?.Name.Length > 256) + { + throw new ArgumentException("Embed author name length must not exceed 256 characters."); + } + + int totalCharacter = 0; + totalCharacter += embed.Title.Length; + totalCharacter += embed.Description?.Length ?? 0; + totalCharacter += embed.Fields?.Sum(xf => xf.Name.Length + xf.Value.Length) ?? 0; + totalCharacter += embed.Footer?.Text.Length ?? 0; + totalCharacter += embed.Author?.Name.Length ?? 0; + if (totalCharacter > 6000) + { + throw new ArgumentException("Embed total length must not exceed 6000 characters."); + } + + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } } - internal Task TriggerTypingAsync(ulong channel_id) + RestChannelMessageCreatePayload pld = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TYPING}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route); - } + HasContent = content != null, + Content = content, + IsTTS = false, + HasEmbed = embeds?.Any() ?? false, + Embeds = embeds, + Flags = suppressNotifications ? MessageFlags.SupressNotifications : 0, + }; - internal async Task> GetPinnedMessagesAsync(ulong channel_id) + if (replyMessageId != null) { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var msgs_raw = JArray.Parse(res.Response); - var msgs = new List(); - foreach (var xj in msgs_raw) - msgs.Add(this.PrepareMessage(xj)); - - return new ReadOnlyCollection(new List(msgs)); + pld.MessageReference = new InternalDiscordMessageReference + { + MessageId = replyMessageId, + FailIfNotExists = failOnInvalidReply + }; } - internal Task PinMessageAsync(ulong channel_id, ulong message_id) + if (replyMessageId != null) { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, message_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route); + pld.Mentions = new DiscordMentions(Mentions.All, true, mentionReply); } - internal Task UnpinMessageAsync(ulong channel_id, ulong message_id) + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + + RestRequest request = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); - } + DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); - internal Task AddGroupDmRecipientAsync(ulong channel_id, ulong user_id, string access_token, string nickname) - { - var pld = new RestChannelGroupDmRecipientAddPayload - { - AccessToken = access_token, - Nickname = nickname - }; + return ret; + } - var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, user_id }, out var path); + internal async ValueTask CreateMessageAsync + ( + ulong channelId, + DiscordMessageBuilder builder + ) + { + builder.Validate(); - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); + if (builder.Embeds != null) + { + foreach (DiscordEmbed embed in builder.Embeds) + { + if (embed?.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } } - internal Task RemoveGroupDmRecipientAsync(ulong channel_id, ulong user_id) + RestChannelMessageCreatePayload pld = new() { - var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, user_id }, out var path); + HasContent = builder.Content != null, + Content = builder.Content, + StickersIds = builder._stickers?.Where(s => s != null).Select(s => s.Id).ToArray(), + IsTTS = builder.IsTTS, + HasEmbed = builder.Embeds != null, + Embeds = builder.Embeds, + Components = builder.Components + }; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); + if (builder.ReplyId != null) + { + pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; } - internal async Task CreateGroupDmAsync(IEnumerable access_tokens, IDictionary nicks) + pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.None, builder.Mentions?.Any() ?? false, builder.MentionOnReply); + + if (builder.Files.Count == 0) { - var pld = new RestUserGroupDmCreatePayload + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + + RestRequest request = new() { - AccessTokens = access_tokens, - Nicknames = nicks + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); - - var ret = DiscordJson.ToDiscordObject(res.Response); - ret.Discord = this._discord; - + DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); + return ret; } - - internal async Task CreateDmAsync(ulong recipient_id) + else { - var pld = new RestUserDmCreatePayload + Dictionary values = new() { - Recipient = recipient_id + ["payload_json"] = DiscordJson.SerializeObject(pld) }; + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Values = values + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); - if (this._discord is DiscordClient dc) - _ = dc._privateChannels.TryAdd(ret.Id, ret); + builder.ResetFileStreamPositions(); return ret; } + } + + internal async ValueTask> GetGuildChannelsAsync(ulong guildId) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable channelsRaw = JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xc => + { + xc.Discord = this._discord!; + return xc; + } + ); - internal async Task FollowChannelAsync(ulong channel_id, ulong webhook_channel_id) + foreach (DiscordChannel? ret in channelsRaw) { - var pld = new FollowedChannelAddPayload + foreach (DiscordOverwrite xo in ret._permissionOverwrites) { - WebhookChannelId = webhook_channel_id - }; - - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.FOLLOWERS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); + xo.Discord = this._discord!; + xo._channel_id = ret.Id; + } + } - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + return new ReadOnlyCollection(new List(channelsRaw)); + } - return JsonConvert.DeserializeObject(response.Response); + internal async ValueTask> GetChannelMessagesAsync + ( + ulong channelId, + int limit, + ulong? before = null, + ulong? after = null, + ulong? around = null + ) + { + QueryUriBuilder url = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"); + if (around is not null) + { + url.AddParameter("around", around?.ToString(CultureInfo.InvariantCulture)); } - internal async Task CrosspostMessageAsync(ulong channel_id, ulong message_id) + if (before is not null) { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.CROSSPOST}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, message_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route); - return JsonConvert.DeserializeObject(response.Response); + url.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); } - internal async Task CreateStageInstanceAsync(ulong channelId, string topic, PrivacyLevel? privacyLevel, string reason) + if (after is not null) { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + url.AddParameter("after", after?.ToString(CultureInfo.InvariantCulture)); + } - var pld = new RestCreateStageInstancePayload - { - ChannelId = channelId, - Topic = topic, - PrivacyLevel = privacyLevel - }; + if (limit > 0) + { + url.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + } - var route = $"{Endpoints.STAGE_INSTANCES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); + JArray msgsRaw = JArray.Parse(res.Response!); + List msgs = new(); + foreach (JToken xj in msgsRaw) + { + msgs.Add(this.PrepareMessage(xj)); + } - var stage = JsonConvert.DeserializeObject(response.Response); - stage.Discord = this._discord; + return new ReadOnlyCollection(new List(msgs)); + } - return stage; - } + internal async ValueTask GetChannelMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; - internal async Task GetStageInstanceAsync(ulong channel_id) + RestRequest request = new() { - var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); - var stage = JsonConvert.DeserializeObject(response.Response); - stage.Discord = this._discord; - - return stage; - } + return ret; + } - internal async Task ModifyStageInstanceAsync(ulong channel_id, Optional topic, Optional privacyLevel, string reason) + internal async ValueTask EditMessageAsync + ( + ulong channelId, + ulong messageId, + Optional content = default, + Optional> embeds = default, + Optional> mentions = default, + IReadOnlyList? components = null, + IReadOnlyCollection? files = null, + MessageFlags? flags = null, + IEnumerable? attachments = null + ) + { + if (embeds.HasValue && embeds.Value != null) { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + foreach (DiscordEmbed embed in embeds.Value) + { + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } + } - var pld = new RestModifyStageInstancePayload + RestChannelMessageEditPayload pld = new() + { + HasContent = content.HasValue, + Content = content.HasValue ? (string)content : null, + HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), + Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, + Components = components, + Flags = flags, + Attachments = attachments, + Mentions = mentions.HasValue + ? new DiscordMentions + ( + mentions.Value ?? Mentions.None, + false, + mentions.Value?.OfType().Any() ?? false + ) + : null + }; + + Dictionary values = new() + { + ["payload_json"] = DiscordJson.SerializeObject(pld) + }; + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; + + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Values = values + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); + + if (files is not null) + { + foreach (DiscordMessageFile? file in files.Where(x => x.ResetPositionTo.HasValue)) { - Topic = topic, - PrivacyLevel = privacyLevel - }; + file.Stream.Position = file.ResetPositionTo!.Value; + } + } - var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); + return ret; + } + + internal async ValueTask DeleteMessageAsync + ( + ulong channelId, + ulong messageId, + string reason + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; - var stage = JsonConvert.DeserializeObject(response.Response); - stage.Discord = this._discord; + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - return stage; - } + internal async ValueTask DeleteMessagesAsync + ( + ulong channelId, + IEnumerable messageIds, + string reason + ) + { + RestChannelMessageBulkDeletePayload pld = new() + { + Messages = messageIds + }; - internal async Task BecomeStageInstanceSpeakerAsync(ulong guildId, ulong id, ulong? userId = null, DateTime? timestamp = null, bool? suppress = null) + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) { - var headers = Utilities.GetBaseHeaders(); + headers[REASON_HEADER_NAME] = reason; + } - var pld = new RestBecomeStageSpeakerInstancePayload - { - Suppress = suppress, - ChannelId = id, - RequestToSpeakTimestamp = timestamp - }; + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{Endpoints.BULK_DELETE}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{Endpoints.BULK_DELETE}"; - var user = userId?.ToString() ?? "@me"; - var route = $"/guilds/{guildId}{Endpoints.VOICE_STATES}/{user}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { id }, out var path); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); + internal async ValueTask> GetChannelInvitesAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable invitesRaw = JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xi => + { + xi.Discord = this._discord!; + return xi; + } + ); - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); - } + return new ReadOnlyCollection(new List(invitesRaw)); + } - internal async Task DeleteStageInstanceAsync(ulong channel_id, string reason) + internal async ValueTask CreateChannelInviteAsync + ( + ulong channelId, + int maxAge, + int maxUses, + bool temporary, + bool unique, + string reason, + InviteTargetType? targetType = null, + ulong? targetUserId = null, + ulong? targetApplicationId = null + ) + { + RestChannelInviteCreatePayload pld = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + MaxAge = maxAge, + MaxUses = maxUses, + Temporary = temporary, + Unique = unique, + TargetType = targetType, + TargetUserId = targetUserId, + TargetApplicationId = targetApplicationId + }; - var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - #endregion + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; - #region Threads - - internal async Task CreateThreadFromMessageAsync(ulong channel_id, ulong message_id, string name, AutoArchiveDuration archiveAfter, string reason) + RestRequest request = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - var payload = new RestThreadCreatePayload - { - Name = name, - ArchiveAfter = archiveAfter - }; + return ret; + } - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.THREADS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, message_id }, out var path); //??? + internal async ValueTask DeleteChannelPermissionAsync + ( + ulong channelId, + ulong overwriteId, + string reason + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(payload)); + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/:overwrite_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/{overwriteId}"; - var thread = JsonConvert.DeserializeObject(response.Response); - thread.Discord = this._discord; + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - return thread; - } + internal async ValueTask EditChannelPermissionsAsync + ( + ulong channelId, + ulong overwriteId, + Permissions allow, + Permissions deny, + string type, + string? reason = null + ) + { + RestChannelPermissionEditPayload pld = new() + { + Type = type, + Allow = allow & PermissionMethods.FULL_PERMS, + Deny = deny & PermissionMethods.FULL_PERMS + }; - internal async Task CreateThreadAsync(ulong channel_id, string name, AutoArchiveDuration archiveAfter, ChannelType type, string reason) + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + headers[REASON_HEADER_NAME] = reason; + } - var payload = new RestThreadCreatePayload - { - Name = name, - ArchiveAfter = archiveAfter, - Type = type - }; + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/:overwrite_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/{overwriteId}"; - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id}, out var path); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(payload)); + internal async ValueTask TriggerTypingAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.TYPING}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.TYPING}"; - var thread = JsonConvert.DeserializeObject(response.Response); - thread.Discord = this._discord; + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post + }; + await this._rest.ExecuteRequestAsync(request); + } - return thread; - } + internal async ValueTask> GetPinnedMessagesAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}"; - internal Task JoinThreadAsync(ulong channel_id) + RestRequest request = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id}, out var path); + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route); + JArray msgsRaw = JArray.Parse(res.Response!); + List msgs = new(); + foreach (JToken xj in msgsRaw) + { + msgs.Add(this.PrepareMessage(xj)); } - internal Task LeaveThreadAsync(ulong channel_id) - { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id}, out var path); + return new ReadOnlyCollection(new List(msgs)); + } - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); - } + internal async ValueTask PinMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put + }; + + await this._rest.ExecuteRequestAsync(request); + } - internal async Task GetThreadMemberAsync(ulong channel_id, ulong user_id) - { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, user_id }, out var path); + internal async ValueTask UnpinMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + internal async ValueTask AddGroupDmRecipientAsync + ( + ulong channelId, + ulong userId, + string accessToken, + string nickname + ) + { + RestChannelGroupDmRecipientAddPayload pld = new() + { + AccessToken = accessToken, + Nickname = nickname + }; + + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/:user_id"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld) + }; + + await this._rest.ExecuteRequestAsync(request); + } - return JsonConvert.DeserializeObject(response.Response); - } + internal async ValueTask RemoveGroupDmRecipientAsync + ( + ulong channelId, + ulong userId + ) + { + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/:user_id"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + await this._rest.ExecuteRequestAsync(request); + } - internal Task AddThreadMemberAsync(ulong channel_id, ulong user_id) + internal async ValueTask CreateGroupDmAsync + ( + IEnumerable accessTokens, + IDictionary nicks + ) + { + RestUserGroupDmCreatePayload pld = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, user_id}, out var path); + AccessTokens = accessTokens, + Nicknames = nicks + }; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route); - } + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; - internal Task RemoveThreadMemberAsync(ulong channel_id, ulong user_id) + RestRequest request = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, user_id}, out var path); + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordDmChannel ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); - } + return ret; + } - internal async Task> ListThreadMembersAsync(ulong channel_id) + internal async ValueTask CreateDmAsync + ( + ulong recipientId + ) + { + RestUserDmCreatePayload pld = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id}, out var path); + Recipient = recipientId + }; - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + string route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; - var threadMembers = JsonConvert.DeserializeObject>(response.Response); - return new ReadOnlyCollection(threadMembers); - } + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordDmChannel ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - internal async Task ListActiveThreadsAsync(ulong guild_id) + if (this._discord is DiscordClient dc) { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.THREADS}{Endpoints.ACTIVE}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id}, out var path); + _ = dc._privateChannels.TryAdd(ret.Id, ret); + } - var url = Utilities.GetApiUriFor(path); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + return ret; + } - var result = JsonConvert.DeserializeObject(response.Response); - result.HasMore = false; + internal async ValueTask FollowChannelAsync + ( + ulong channelId, + ulong webhookChannelId + ) + { + FollowedChannelAddPayload pld = new() + { + WebhookChannelId = webhookChannelId + }; + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.FOLLOWERS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.FOLLOWERS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } - foreach (var thread in result.Threads) - thread.Discord = this._discord; - foreach (var member in result.Members) - { - member.Discord = this._discord; - member._guild_id = guild_id; - var thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread != null) - thread.CurrentMember = member; - } + internal async ValueTask CrosspostMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.CROSSPOST}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.CROSSPOST}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } - return result; + internal async ValueTask CreateStageInstanceAsync + ( + ulong channelId, + string topic, + PrivacyLevel? privacyLevel = null, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task ListPublicArchivedThreadsAsync(ulong guild_id, ulong channel_id, string before, int limit) + RestCreateStageInstancePayload pld = new() { - var queryParams = new Dictionary(); - if (before != null) - queryParams["before"] = before?.ToString(CultureInfo.InvariantCulture); - if (limit > 0) - queryParams["limit"] = limit.ToString(CultureInfo.InvariantCulture); + ChannelId = channelId, + Topic = topic, + PrivacyLevel = privacyLevel + }; - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.ARCHIVED}{Endpoints.PUBLIC}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id}, out var path); + string route = $"{Endpoints.STAGE_INSTANCES}"; + string url = $"{Endpoints.STAGE_INSTANCES}"; - var url = Utilities.GetApiUriFor(path, queryParams.Any() ? BuildQueryString(queryParams) : ""); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); - var result = JsonConvert.DeserializeObject(response.Response); + DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; + stage.Discord = this._discord!; - foreach (var thread in result.Threads) - thread.Discord = this._discord; - foreach (var member in result.Members) - { - member.Discord = this._discord; - member._guild_id = guild_id; - var thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread != null) - thread.CurrentMember = member; - } + return stage; + } - return result; - } + internal async ValueTask GetStageInstanceAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; - internal async Task ListPrivateArchivedThreadsAsync(ulong guild_id, ulong channel_id, string before, int limit) + RestRequest request = new() { - var queryParams = new Dictionary(); - if (before != null) - queryParams["before"] = before?.ToString(CultureInfo.InvariantCulture); - if (limit > 0) - queryParams["limit"] = limit.ToString(CultureInfo.InvariantCulture); - - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.ARCHIVED}{Endpoints.PRIVATE}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id}, out var path); + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path, queryParams.Any() ? BuildQueryString(queryParams) : ""); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; + stage.Discord = this._discord!; - var result = JsonConvert.DeserializeObject(response.Response); - - foreach (var thread in result.Threads) - thread.Discord = this._discord; - foreach (var member in result.Members) - { - member.Discord = this._discord; - member._guild_id = guild_id; - var thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread != null) - thread.CurrentMember = member; - } + return stage; + } - return result; + internal async ValueTask ModifyStageInstanceAsync + ( + ulong channelId, + Optional topic = default, + Optional privacyLevel = default, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task ListJoinedPrivateArchivedThreadsAsync(ulong guild_id, ulong channel_id, ulong? before, int limit) + RestModifyStageInstancePayload pld = new() { - var queryParams = new Dictionary(); - if (before != null) - queryParams["before"] = before?.ToString(CultureInfo.InvariantCulture); - if (limit > 0) - queryParams["limit"] = limit.ToString(CultureInfo.InvariantCulture); + Topic = topic, + PrivacyLevel = privacyLevel + }; - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.USERS}{Endpoints.ME}{Endpoints.THREADS}{Endpoints.ARCHIVED}{Endpoints.PUBLIC}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id}, out var path); + string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; - var url = Utilities.GetApiUriFor(path, queryParams.Any() ? BuildQueryString(queryParams) : ""); - var response = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); + DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; + stage.Discord = this._discord!; - var result = JsonConvert.DeserializeObject(response.Response); + return stage; + } - foreach (var thread in result.Threads) - thread.Discord = this._discord; - foreach (var member in result.Members) - { - member.Discord = this._discord; - member._guild_id = guild_id; - var thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread != null) - thread.CurrentMember = member; - } + internal async ValueTask BecomeStageInstanceSpeakerAsync + ( + ulong guildId, + ulong id, + ulong? userId = null, + DateTime? timestamp = null, + bool? suppress = null + ) + { + Dictionary headers = new(); - return result; - } + RestBecomeStageSpeakerInstancePayload pld = new() + { + Suppress = suppress, + ChannelId = id, + RequestToSpeakTimestamp = timestamp + }; - #endregion + string user = userId?.ToString() ?? "@me"; + string route = $"/guilds/{guildId}/{Endpoints.VOICE_STATES}/{(userId is null ? "@me" : ":user_id")}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{user}"; - #region Member - internal Task GetCurrentUserAsync() - => this.GetUserAsync("@me"); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - internal Task GetUserAsync(ulong user_id) - => this.GetUserAsync(user_id.ToString(CultureInfo.InvariantCulture)); + internal async ValueTask DeleteStageInstanceAsync + ( + ulong channelId, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } - internal async Task GetUserAsync(string user_id) + string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + + RestRequest request = new() { - var route = $"{Endpoints.USERS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { user_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + #endregion - var user_raw = JsonConvert.DeserializeObject(res.Response); - var duser = new DiscordUser(user_raw) { Discord = this._discord }; + #region Threads - return duser; + internal async ValueTask CreateThreadFromMessageAsync + ( + ulong channelId, + ulong messageId, + string name, + AutoArchiveDuration archiveAfter, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task GetGuildMemberAsync(ulong guild_id, ulong user_id) + RestThreadCreatePayload payload = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, user_id }, out var path); + Name = name, + ArchiveAfter = archiveAfter + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.THREADS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.THREADS}"; - var tm = JsonConvert.DeserializeObject(res.Response); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(payload), + Headers = headers + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); - var usr = new DiscordUser(tm.User) { Discord = this._discord }; - usr = this._discord.UpdateUserCache(usr); + DiscordThreadChannel thread = JsonConvert.DeserializeObject(response.Response!)!; + thread.Discord = this._discord!; - return new DiscordMember(tm) - { - Discord = this._discord, - _guild_id = guild_id - }; - } + return thread; + } - internal Task RemoveGuildMemberAsync(ulong guild_id, ulong user_id, string reason) + internal async ValueTask CreateThreadAsync + ( + ulong channelId, + string name, + AutoArchiveDuration archiveAfter, + ChannelType type, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) { - var urlparams = new Dictionary(); - if (reason != null) - urlparams["reason"] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path); - - var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams)); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); + headers[REASON_HEADER_NAME] = reason; } - internal async Task ModifyCurrentUserAsync(string username, Optional base64_avatar) + RestThreadCreatePayload payload = new() { - var pld = new RestUserUpdateCurrentPayload - { - Username = username, - AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, - AvatarSet = base64_avatar.HasValue - }; - - var route = $"{Endpoints.USERS}{Endpoints.ME}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { }, out var path); + Name = name, + ArchiveAfter = archiveAfter, + Type = type + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(payload), + Headers = headers + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); - var user_raw = JsonConvert.DeserializeObject(res.Response); + DiscordThreadChannel thread = JsonConvert.DeserializeObject(response.Response!)!; + thread.Discord = this._discord!; - return user_raw; - } + return thread; + } - internal async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) - { - var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}"; + internal async ValueTask JoinThreadAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put + }; + + await this._rest.ExecuteRequestAsync(request); + } - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); + internal async ValueTask LeaveThreadAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriBuilderFor(path) - .AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); + internal async ValueTask GetThreadMemberAsync + ( + ulong channelId, + ulong userId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); + + DiscordThreadChannelMember ret = JsonConvert.DeserializeObject(response.Response!)!; + + ret.Discord = this._discord!; + + return ret; + } - if (before != null) - url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); - if (after != null) - url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + internal async ValueTask AddThreadMemberAsync + ( + ulong channelId, + ulong userId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put + }; + + await this._rest.ExecuteRequestAsync(request); + } - var res = await this.DoRequestAsync(this._discord, bucket, url.Build(), RestRequestMethod.GET, route); + internal async ValueTask RemoveThreadMemberAsync + ( + ulong channelId, + ulong userId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this._rest.ExecuteRequestAsync(request); + } - if (this._discord is DiscordClient) - { - var guilds_raw = JsonConvert.DeserializeObject>(res.Response); - var glds = guilds_raw.Select(xug => (this._discord as DiscordClient)?._guilds[xug.Id]); - return new ReadOnlyCollection(new List(glds)); - } - else - { - return new ReadOnlyCollection(JsonConvert.DeserializeObject>(res.Response)); - } - } + internal async ValueTask> ListThreadMembersAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); + + List threadMembers = JsonConvert.DeserializeObject>(response.Response!)!; + return new ReadOnlyCollection(threadMembers); + } - internal Task ModifyGuildMemberAsync(ulong guild_id, ulong user_id, Optional nick, - Optional> role_ids, Optional mute, Optional deaf, - Optional voice_channel_id, Optional communication_disabled_until, string reason) + internal async ValueTask ListActiveThreadsAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.THREADS}/{Endpoints.ACTIVE}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.THREADS}/{Endpoints.ACTIVE}"; + + RestRequest request = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var pld = new RestGuildMemberModifyPayload - { - Nickname = nick, - RoleIds = role_ids, - Deafen = deaf, - Mute = mute, - VoiceChannelId = voice_channel_id, - CommunicationDisabledUntil = communication_disabled_until - }; + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); + ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; + result.HasMore = false; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); + foreach (DiscordThreadChannel thread in result.Threads) + { + thread.Discord = this._discord!; } - internal Task ModifyCurrentMemberAsync(ulong guild_id, string nick, string reason) + foreach (DiscordThreadChannelMember member in result.Members) { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var pld = new RestGuildMemberModifyPayload + member.Discord = this._discord!; + member._guild_id = guildId; + DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread is not null) { - Nickname = nick - }; + thread.CurrentMember = member; + } + } - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.ME}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); + return result; + } - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); + internal async ValueTask ListPublicArchivedThreadsAsync + ( + ulong guildId, + ulong channelId, + string before, + int limit + ) + { + QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"); + if (before != null) + { + queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); } - #endregion - #region Roles - internal async Task> GetGuildRolesAsync(ulong guild_id) + if (limit > 0) { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var roles_raw = JsonConvert.DeserializeObject>(res.Response).Select(xr => { xr.Discord = this._discord; xr._guild_id = guild_id; return xr; }); - - return new ReadOnlyCollection(new List(roles_raw)); + queryParams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); } - internal async Task GetGuildAsync(ulong guildId, bool? with_counts) + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"; + + + RestRequest request = new() { - var urlparams = new Dictionary(); - if (with_counts.HasValue) - urlparams["with_counts"] = with_counts?.ToString(); - - var route = $"{Endpoints.GUILDS}/:guild_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); + Route = route, + Url = queryParams.Build(), + Method = HttpMethod.Get, + + }; - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : ""); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route, urlparams); + RestResponse response = await this._rest.ExecuteRequestAsync(request); - var json = JObject.Parse(res.Response); - var rawMembers = (JArray)json["members"]; - var guildRest = json.ToDiscordObject(); - foreach (var r in guildRest._roles.Values) - r._guild_id = guildRest.Id; + ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; - if (this._discord is DiscordClient dc) - { - await dc.OnGuildUpdateEventAsync(guildRest, rawMembers); - return dc._guilds[guildRest.Id]; - } - else - { - guildRest.Discord = this._discord; - return guildRest; - } + foreach (DiscordThreadChannel thread in result.Threads) + { + thread.Discord = this._discord!; } - internal async Task ModifyGuildRoleAsync(ulong guild_id, ulong role_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason, Stream icon, string emoji) + foreach (DiscordThreadChannelMember member in result.Members) { - string image = null; - - if (icon != null) + member.Discord = this._discord!; + member._guild_id = guildId; + DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread is not null) { - using var it = new ImageTool(icon); - image = it.GetBase64(); + thread.CurrentMember = member; } + } - var pld = new RestGuildRolePayload - { - Name = name, - Permissions = permissions & PermissionMethods.FULL_PERMS, - Color = color, - Hoist = hoist, - Mentionable = mentionable, - Emoji = emoji, - Icon = image - }; - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, role_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + return result; + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - ret._guild_id = guild_id; + internal async ValueTask ListPrivateArchivedThreadsAsync + ( + ulong guildId, + ulong channelId, + int limit, + string? before = null + ) + { + QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}"); + if (before is not null) + { + queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); + } - return ret; + if (limit > 0) + { + queryParams.AddParameter("limit",limit.ToString(CultureInfo.InvariantCulture)); } - internal Task DeleteRoleAsync(ulong guild_id, ulong role_id, string reason) + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}"; + + RestRequest request = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = route, + Url = queryParams.Build(), + Method = HttpMethod.Get + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, role_id }, out var path); + ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + foreach (DiscordThreadChannel thread in result.Threads) + { + thread.Discord = this._discord!; } - internal async Task CreateGuildRoleAsync(ulong guild_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason, Stream icon, string emoji) + foreach (DiscordThreadChannelMember member in result.Members) { - string image = null; - - if (icon != null) + member.Discord = this._discord!; + member._guild_id = guildId; + DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread is not null) { - using var it = new ImageTool(icon); - image = it.GetBase64(); + thread.CurrentMember = member; } + } - var pld = new RestGuildRolePayload - { - Name = name, - Permissions = permissions & PermissionMethods.FULL_PERMS, - Color = color, - Hoist = hoist, - Mentionable = mentionable, - Emoji = emoji, - Icon = image - }; - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); + return result; + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - ret._guild_id = guild_id; + internal async ValueTask ListJoinedPrivateArchivedThreadsAsync + ( + ulong guildId, + ulong channelId, + int limit, + ulong? before = null + ) + { + QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}/{Endpoints.ME}"); + if (before is not null) + { + queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); + } - return ret; + if (limit > 0) + { + queryParams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); } - #endregion - #region Prune - internal async Task GetGuildPruneCountAsync(ulong guild_id, int days, IEnumerable include_roles) + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"; + + RestRequest request = new() { - if (days < 0 || days > 30) - throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); + Route = route, + Url = queryParams.Build(), + Method = HttpMethod.Get + }; + + RestResponse response = await this._rest.ExecuteRequestAsync(request); - var urlparams = new Dictionary - { - ["days"] = days.ToString(CultureInfo.InvariantCulture) - }; + ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; - var sb = new StringBuilder(); + foreach (DiscordThreadChannel thread in result.Threads) + { + thread.Discord = this._discord!; + } - if (include_roles != null) + foreach (DiscordThreadChannelMember member in result.Members) + { + member.Discord = this._discord!; + member._guild_id = guildId; + DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread is not null) { - var roleArray = include_roles.ToArray(); - var roleArrayCount = roleArray.Count(); - - for (var i = 0; i < roleArrayCount; i++) - sb.Append($"&include_roles={roleArray[i]}"); + thread.CurrentMember = member; } - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}"); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var pruned = JsonConvert.DeserializeObject(res.Response); - - return pruned.Pruned.Value; } - internal async Task BeginGuildPruneAsync(ulong guild_id, int days, bool compute_prune_count, IEnumerable include_roles, string reason) - { - if (days < 0 || days > 30) - throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); + return result; + } - var urlparams = new Dictionary - { - ["days"] = days.ToString(CultureInfo.InvariantCulture), - ["compute_prune_count"] = compute_prune_count.ToString() - }; + #endregion - var sb = new StringBuilder(); + #region Member + internal ValueTask GetCurrentUserAsync() + => this.GetUserAsync("@me"); - if (include_roles != null) - { - var roleArray = include_roles.ToArray(); - var roleArrayCount = roleArray.Count(); + internal ValueTask GetUserAsync(ulong userId) + => this.GetUserAsync(userId.ToString(CultureInfo.InvariantCulture)); - for (var i = 0; i < roleArrayCount; i++) - sb.Append($"&include_roles={roleArray[i]}"); - } + internal async ValueTask GetUserAsync(string userId) + { + string route = $"{Endpoints.USERS}/:user_id"; + string url = $"{Endpoints.USERS}/{userId}"; - var headers = Utilities.GetBaseHeaders(); - if (string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); + TransportUser userRaw = JsonConvert.DeserializeObject(res.Response!)!; + DiscordUser user = new(userRaw) + { + Discord = this._discord! + }; - var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}"); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers); + return user; + } - var pruned = JsonConvert.DeserializeObject(res.Response); + internal async ValueTask GetGuildMemberAsync + ( + ulong guildId, + ulong userId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + TransportMember tm = JsonConvert.DeserializeObject(res.Response!)!; + + DiscordUser usr = new(tm.User) + { + Discord = this._discord ! + }; + _ = this._discord!.UpdateUserCache(usr); + + return new DiscordMember(tm) + { + Discord = this._discord, + _guild_id = guildId + }; + } - return pruned.Pruned; + internal async ValueTask RemoveGuildMemberAsync + ( + ulong guildId, + ulong userId, + string? reason = null + ) + { + QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"); + if (reason != null) + { + urlparams.AddParameter("reason", reason); } - #endregion - #region GuildVarious - internal async Task GetTemplateAsync(string code) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; + + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:code"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { code }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var templates_raw = JsonConvert.DeserializeObject(res.Response); + Route = route, + Url = urlparams.Build(), + Method = HttpMethod.Delete + }; + + await this._rest.ExecuteRequestAsync(request); + } - return templates_raw; - } + internal async ValueTask ModifyCurrentUserAsync + ( + string username, + Optional base64Avatar = default + ) + { + RestUserUpdateCurrentPayload pld = new() + { + Username = username, + AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, + AvatarSet = base64Avatar.HasValue + }; - internal async Task> GetGuildIntegrationsAsync(ulong guild_id) + string route = $"{Endpoints.USERS}/{Endpoints.ME}"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}"; + + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + TransportUser userRaw = JsonConvert.DeserializeObject(res.Response!)!; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + return userRaw; + } - var integrations_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this._discord; return xi; }); + internal async ValueTask> GetCurrentUserGuildsAsync + ( + int limit = 100, + ulong? before = null, + ulong? after = null + ) + { + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}"; + QueryUriBuilder url = new($"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}"); + url.AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); - return new ReadOnlyCollection(new List(integrations_raw)); + if (before != null) + { + url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); } - internal async Task GetGuildPreviewAsync(ulong guild_id) + if (after != null) + { + url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PREVIEW}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Route = route, + Url = url.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + if (this._discord is DiscordClient) + { + IEnumerable guildsRaw = JsonConvert.DeserializeObject>(res.Response!)!; + IEnumerable guilds = guildsRaw.Select + ( + xug => (this._discord as DiscordClient)?._guilds[xug.Id] + ) + .Where(static guild => guild is not null)!; + return new ReadOnlyCollection(new List(guilds)); + } + else + { + return new ReadOnlyCollection(JsonConvert.DeserializeObject>(res.Response!)!); + } + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + internal async ValueTask ModifyGuildMemberAsync + ( + ulong guildId, + ulong userId, + Optional nick = default, + Optional> roleIds = default, + Optional mute = default, + Optional deaf = default, + Optional voiceChannelId = default, + Optional communicationDisabledUntil = default, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestGuildMemberModifyPayload pld = new() + { + Nickname = nick, + RoleIds = roleIds, + Deafen = deaf, + Mute = mute, + VoiceChannelId = voiceChannelId, + CommunicationDisabledUntil = communicationDisabledUntil + }; + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - return ret; + internal async ValueTask ModifyCurrentMemberAsync + ( + ulong guildId, + string nick, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task CreateGuildIntegrationAsync(ulong guild_id, string type, ulong id) + RestGuildMemberModifyPayload pld = new() { - var pld = new RestGuildIntegrationAttachPayload - { - Type = type, - Id = id - }; + Nickname = nick + }; - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.ME}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } + #endregion - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + #region Roles + internal async ValueTask> GetGuildRolesAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable rolesRaw = JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xr => + { + xr.Discord = this._discord!; + xr._guild_id = guildId; + return xr; + } + ); - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + return new ReadOnlyCollection(new List(rolesRaw)); + } - return ret; + internal async ValueTask GetGuildAsync + ( + ulong guildId, + bool? withCounts + ) + { + QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}"); + if (withCounts.HasValue) + { + urlparams.AddParameter("with_counts", withCounts?.ToString()); } - internal async Task ModifyGuildIntegrationAsync(ulong guild_id, ulong integration_id, int expire_behaviour, int expire_grace_period, bool enable_emoticons) + string route = $"{Endpoints.GUILDS}/{guildId}"; + + RestRequest request = new() { - var pld = new RestGuildIntegrationModifyPayload - { - ExpireBehavior = expire_behaviour, - ExpireGracePeriod = expire_grace_period, - EnableEmoticons = enable_emoticons - }; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, integration_id }, out var path); + Route = route, + Url = urlparams.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); - - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - - return ret; + JObject json = JObject.Parse(res.Response!); + JArray rawMembers = (JArray)json["members"]!; + DiscordGuild guildRest = json.ToDiscordObject(); + foreach (DiscordRole role in guildRest._roles.Values) + { + role._guild_id = guildRest.Id; } - internal Task DeleteGuildIntegrationAsync(ulong guild_id, DiscordIntegration integration, string reason) + if (this._discord is DiscordClient discordClient) { - var headers = Utilities.GetBaseHeaders(); - if (string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + await discordClient.OnGuildUpdateEventAsync(guildRest, rawMembers); + return discordClient._guilds[guildRest.Id]; + } + else + { + guildRest.Discord = this._discord!; + return guildRest; + } + } - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, integration_id = integration.Id }, out var path); + internal async ValueTask ModifyGuildRoleAsync + ( + ulong guildId, + ulong roleId, + string? name = null, + Permissions? permissions = null, + int? color = null, + bool? hoist = null, + bool? mentionable = null, + Stream? icon = null, + string? emoji = null, + string? reason = null + ) + { + string? image = null; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers, DiscordJson.SerializeObject(integration)); + if (icon != null) + { + using ImageTool it = new(icon); + image = it.GetBase64(); } - internal Task SyncGuildIntegrationAsync(ulong guild_id, ulong integration_id) + RestGuildRolePayload pld = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id{Endpoints.SYNC}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id, integration_id }, out var path); + Name = name, + Permissions = permissions & PermissionMethods.FULL_PERMS, + Color = color, + Hoist = hoist, + Mentionable = mentionable, + Emoji = emoji, + Icon = image + }; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route); + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task> GetGuildVoiceRegionsAsync(ulong guild_id) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/:role_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{roleId}"; + + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.REGIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + DiscordRole ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + ret._guild_id = guildId; - var regions_raw = JsonConvert.DeserializeObject>(res.Response); + return ret; + } - return new ReadOnlyCollection(new List(regions_raw)); + internal async ValueTask DeleteRoleAsync + ( + ulong guildId, + ulong roleId, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task> GetGuildInvitesAsync(ulong guild_id) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/:role_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{roleId}"; + + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INVITES}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + internal async ValueTask CreateGuildRoleAsync + ( + ulong guildId, + string name, + Permissions? permissions = null, + int? color = null, + bool? hoist = null, + bool? mentionable = null, + Stream? icon = null, + string? emoji = null, + string? reason = null + ) + { + string? image = null; + + if (icon != null) + { + using ImageTool it = new(icon); + image = it.GetBase64(); + } - var invites_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this._discord; return xi; }); + RestGuildRolePayload pld = new() + { + Name = name, + Permissions = permissions & PermissionMethods.FULL_PERMS, + Color = color, + Hoist = hoist, + Mentionable = mentionable, + Emoji = emoji, + Icon = image + }; - return new ReadOnlyCollection(new List(invites_raw)); + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - #endregion - #region Invite - internal async Task GetInviteAsync(string invite_code, bool? with_counts, bool? with_expiration) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; + + RestRequest request = new() { - var urlparams = new Dictionary(); - if (with_counts.HasValue) - { - urlparams["with_counts"] = with_counts?.ToString(); - urlparams["with_expiration"] = with_expiration?.ToString(); - } + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.INVITES}/:invite_code"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { invite_code }, out var path); + DiscordRole ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + ret._guild_id = guildId; - var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : ""); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + return ret; + } + #endregion + + #region Prune + internal async ValueTask GetGuildPruneCountAsync + ( + ulong guildId, + int days, + IEnumerable? includeRoles = null + ) + { + if (days < 0 || days > 30) + { + throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); + } + + QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"); + urlparams.AddParameter("days", days.ToString(CultureInfo.InvariantCulture)); + + StringBuilder sb = new(); - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + if (includeRoles is not null) + { + ulong[] roleArray = includeRoles.ToArray(); + int roleArrayCount = roleArray.Length; - return ret; + for (int i = 0; i < roleArrayCount; i++) + { + sb.Append($"&include_roles={roleArray[i]}"); + } } - internal async Task DeleteInviteAsync(string invite_code, string reason) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"; + + RestRequest request = new() { - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = route, + Url = urlparams.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.INVITES}/:invite_code"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { invite_code }, out var path); + RestGuildPruneResultPayload pruned = JsonConvert.DeserializeObject(res.Response!)!; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + return pruned.Pruned!.Value; + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + internal async ValueTask BeginGuildPruneAsync + ( + ulong guildId, + int days, + bool computePruneCount, + IEnumerable? includeRoles = null, + string? reason = null + ) + { + if (days < 0 || days > 30) + { + throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); + } + + QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"); + urlparams.AddParameter("days", days.ToString(CultureInfo.InvariantCulture)); + urlparams.AddParameter("compute_prune_count", computePruneCount.ToString()); + + StringBuilder sb = new(); - return ret; + if (includeRoles is not null) + { + foreach (ulong id in includeRoles) + { + sb.Append($"&include_roles={id}"); + } } - /* - * Disabled due to API restrictions - * - * internal async Task InternalAcceptInvite(string invite_code) - * { - * this.Discord.DebugLogger.LogMessage(LogLevel.Warning, "REST API", "Invite accept endpoint was used; this account is now likely unverified", DateTime.Now); - * - * var url = new Uri($"{Utils.GetApiBaseUri(), Endpoints.INVITES}/{invite_code)); - * var bucket = this.Rest.GetBucket(0, MajorParameterType.Unbucketed, url, HttpRequestMethod.POST); - * var res = await this.DoRequestAsync(this.Discord, bucket, url, HttpRequestMethod.POST); - * - * var ret = JsonConvert.DeserializeObject(res.Response); - * ret.Discord = this.Discord; - * - * return ret; - * } - */ - #endregion + Dictionary headers = new(); + if (string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason!; + } - #region Connections - internal async Task> GetUsersConnectionsAsync() + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"; + + RestRequest request = new() { - var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); + Route = route, + Url = urlparams.Build() + sb.ToString(), + Method = HttpMethod.Post, + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + RestGuildPruneResultPayload pruned = JsonConvert.DeserializeObject(res.Response!)!; - var connections_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this._discord; return xc; }); + return pruned.Pruned; + } + #endregion - return new ReadOnlyCollection(new List(connections_raw)); - } - #endregion + #region GuildVarious + internal async ValueTask GetTemplateAsync + ( + string code + ) + { + string route = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/:code"; + string url = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/{code}"; - #region Voice - internal async Task> ListVoiceRegionsAsync() + RestRequest request = new() { - var route = $"{Endpoints.VOICE}{Endpoints.REGIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + DiscordGuildTemplate templatesRaw = JsonConvert.DeserializeObject(res.Response!)!; - var regions = JsonConvert.DeserializeObject>(res.Response); + return templatesRaw; + } - return new ReadOnlyCollection(new List(regions)); - } - #endregion + internal async ValueTask> GetGuildIntegrationsAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable integrationsRaw = + JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xi => + { + xi.Discord = this._discord!; + return xi; + } + ); + + return new ReadOnlyCollection(new List(integrationsRaw)); + } - #region Webhooks - internal async Task CreateWebhookAsync(ulong channel_id, string name, Optional base64_avatar, string reason) + internal async ValueTask GetGuildPreviewAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PREVIEW}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PREVIEW}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordGuildPreview ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + + return ret; + } + + internal async ValueTask CreateGuildIntegrationAsync + ( + ulong guildId, + string type, + ulong id + ) + { + RestGuildIntegrationAttachPayload pld = new() { - var pld = new RestWebhookPayload - { - Name = name, - AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, - AvatarSet = base64_avatar.HasValue - }; + Type = type, + Id = id + }; - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); + DiscordIntegration ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); + return ret; + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - ret.ApiClient = this; + internal async ValueTask ModifyGuildIntegrationAsync + ( + ulong guildId, + ulong integrationId, + int expireBehaviour, + int expireGracePeriod, + bool enableEmoticons + ) + { + RestGuildIntegrationModifyPayload pld = new() + { + ExpireBehavior = expireBehaviour, + ExpireGracePeriod = expireGracePeriod, + EnableEmoticons = enableEmoticons + }; + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordIntegration ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + + return ret; + } - return ret; + internal async ValueTask DeleteGuildIntegrationAsync + ( + ulong guildId, + ulong integrationId, + string? reason = null + ) + { + Dictionary headers = new(); + if (string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason!; } - internal async Task> GetChannelWebhooksAsync(ulong channel_id) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}"; + + RestRequest request = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + internal async ValueTask SyncGuildIntegrationAsync + ( + ulong guildId, + ulong integrationId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id/{Endpoints.SYNC}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}/{Endpoints.SYNC}"; - var webhooks_raw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this._discord; xw.ApiClient = this; return xw; }); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post + }; + + await this._rest.ExecuteRequestAsync(request); + } - return new ReadOnlyCollection(new List(webhooks_raw)); - } + internal async ValueTask> GetGuildVoiceRegionsAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.REGIONS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.REGIONS}"; - internal async Task> GetGuildWebhooksAsync(ulong guild_id) + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WEBHOOKS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable regionsRaw = JsonConvert.DeserializeObject>(res.Response!)!; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + return new ReadOnlyCollection(new List(regionsRaw)); + } - var webhooks_raw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this._discord; xw.ApiClient = this; return xw; }); + internal async ValueTask> GetGuildInvitesAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INVITES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INVITES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable invitesRaw = + JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xi => + { + xi.Discord = this._discord!; + return xi; + } + ); - return new ReadOnlyCollection(new List(webhooks_raw)); + return new ReadOnlyCollection(new List(invitesRaw)); + } + #endregion + + #region Invite + internal async ValueTask GetInviteAsync + ( + string inviteCode, + bool? withCounts = null, + bool? withExpiration = null + ) + { + Dictionary urlparams = new(); + if (withCounts.HasValue) + { + urlparams["with_counts"] = withCounts?.ToString()!; + urlparams["with_expiration"] = withExpiration?.ToString()!; } - internal async Task GetWebhookAsync(ulong webhook_id) + string route = $"{Endpoints.INVITES}/:invite_code"; + string url = $"{Endpoints.INVITES}/{inviteCode}"; + + RestRequest request = new() { - var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - ret.ApiClient = this; + return ret; + } - return ret; + internal async ValueTask DeleteInviteAsync + ( + string inviteCode, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - // Auth header not required - internal async Task GetWebhookWithTokenAsync(ulong webhook_id, string webhook_token) + string route = $"{Endpoints.INVITES}/:invite_code"; + string url = $"{Endpoints.INVITES}/{inviteCode}"; + + RestRequest request = new() { - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id, webhook_token }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Token = webhook_token; - ret.Id = webhook_id; - ret.Discord = this._discord; - ret.ApiClient = this; + return ret; + } + #endregion - return ret; - } + #region Connections + internal async ValueTask> GetUsersConnectionsAsync() + { + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CONNECTIONS}"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CONNECTIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable connectionsRaw = + JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xc => + { + xc.Discord = this._discord!; + return xc; + } + ); - internal async Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id) - { - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id, webhook_token, message_id }, out var path); + return new ReadOnlyCollection(new List(connectionsRaw)); + } + #endregion - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + #region Voice + internal async ValueTask> ListVoiceRegionsAsync() + { + string route = $"{Endpoints.VOICE}/{Endpoints.REGIONS}"; + string url = $"{Endpoints.VOICE}/{Endpoints.REGIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable regions = + JsonConvert.DeserializeObject>(res.Response!)!; + + return new ReadOnlyCollection(new List(regions)); + } + #endregion + + #region Webhooks + internal async ValueTask CreateWebhookAsync + ( + ulong channelId, + string name, + Optional base64Avatar = default, + string? reason = null + ) + { + RestWebhookPayload pld = new() + { + Name = name, + AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, + AvatarSet = base64Avatar.HasValue + }; - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - return ret; + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task ModifyWebhookAsync(ulong webhook_id, ulong channelId, string name, Optional base64_avatar, string reason) + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; + + RestRequest request = new() { - var pld = new RestWebhookPayload - { - Name = name, - AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, - AvatarSet = base64_avatar.HasValue, - ChannelId = channelId - }; + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + ret.ApiClient = this; - var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id }, out var path); + return ret; + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + internal async ValueTask> GetChannelWebhooksAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable webhooksRaw = + JsonConvert + .DeserializeObject>(res.Response!)! + .Select + ( + xw => + { + xw.Discord = this._discord!; + xw.ApiClient = this; + return xw; + } + ); - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - ret.ApiClient = this; + return new ReadOnlyCollection(new List(webhooksRaw)); + } - return ret; - } + internal async ValueTask> GetGuildWebhooksAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WEBHOOKS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WEBHOOKS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable webhooksRaw = + JsonConvert + .DeserializeObject>(res.Response!)! + .Select + ( + xw => + { + xw.Discord = this._discord!; + xw.ApiClient = this; + return xw; + } + ); - internal async Task ModifyWebhookAsync(ulong webhook_id, string name, string base64_avatar, string webhook_token, string reason) - { - var pld = new RestWebhookPayload - { - Name = name, - AvatarBase64 = base64_avatar - }; + return new ReadOnlyCollection(new List(webhooksRaw)); + } - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + internal async ValueTask GetWebhookAsync + ( + ulong webhookId + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + ret.ApiClient = this; + + return ret; + } - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id, webhook_token }, out var path); + // Auth header not required + internal async ValueTask GetWebhookWithTokenAsync + ( + ulong webhookId, + string webhookToken + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; + + RestRequest request = new() + { + Route = route, + Url = url, + IsExemptFromGlobalLimit = true, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Token = webhookToken; + ret.Id = webhookId; + ret.Discord = this._discord!; + ret.ApiClient = this; + + return ret; + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + internal async ValueTask GetWebhookMessageAsync + ( + ulong webhookId, + string webhookToken, + ulong messageId + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + IsExemptFromGlobalLimit = true, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + return ret; + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - ret.ApiClient = this; + internal async ValueTask ModifyWebhookAsync + ( + ulong webhookId, + ulong channelId, + string? name = null, + Optional base64Avatar = default, + string? reason = null + ) + { + RestWebhookPayload pld = new() + { + Name = name, + AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, + AvatarSet = base64Avatar.HasValue, + ChannelId = channelId + }; - return ret; + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal Task DeleteWebhookAsync(ulong webhook_id, string reason) + string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; + + RestRequest request = new() { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id }, out var path); + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + ret.ApiClient = this; - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); - } + return ret; + } - internal Task DeleteWebhookAsync(ulong webhook_id, string webhook_token, string reason) + internal async ValueTask ModifyWebhookAsync + ( + ulong webhookId, + string webhookToken, + string? name = null, + string? base64Avatar = null, + string? reason = null + ) + { + RestWebhookPayload pld = new() { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Name = name, + AvatarBase64 = base64Avatar + }; - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id, webhook_token }, out var path); - - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task ExecuteWebhookAsync(ulong webhook_id, string webhook_token, DiscordWebhookBuilder builder) + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; + + RestRequest request = new() { - builder.Validate(); + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + IsExemptFromGlobalLimit = true, + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - if (builder.Embeds != null) - foreach (var embed in builder.Embeds) - if (embed.Timestamp != null) - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - - var values = new Dictionary(); - var pld = new RestWebhookExecutePayload - { - Content = builder.Content, - Username = builder.Username.HasValue ? builder.Username.Value : null, - AvatarUrl = builder.AvatarUrl.HasValue ? builder.AvatarUrl.Value : null, - IsTTS = builder.IsTTS, - Embeds = builder.Embeds, - Components = builder.Components, - }; + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + ret.ApiClient = this; - if (builder.Mentions != null) - pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); + return ret; + } - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null) - values["payload_json"] = DiscordJson.SerializeObject(pld); + internal async ValueTask DeleteWebhookAsync + ( + ulong webhookId, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); + string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = builder.ThreadId == null - ? Utilities.GetApiUriBuilderFor(path).AddParameter("wait", "true").Build() - : Utilities.GetApiUriBuilderFor(path).AddParameter("wait", "true").AddParameter("thread_id", builder.ThreadId.ToString()).Build(); + internal async ValueTask DeleteWebhookAsync + ( + ulong webhookId, + string webhookToken, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } - var res = await this.DoMultipartAsync(this._discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); - var ret = JsonConvert.DeserializeObject(res.Response); + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + IsExemptFromGlobalLimit = true, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - builder.ResetFileStreamPositions(); + internal async ValueTask ExecuteWebhookAsync + ( + ulong webhookId, + string webhookToken, + DiscordWebhookBuilder builder + ) + { + builder.Validate(); - ret.Discord = this._discord; - return ret; + if (builder.Embeds != null) + { + foreach (DiscordEmbed embed in builder.Embeds) + { + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } } - internal async Task ExecuteWebhookSlackAsync(ulong webhook_id, string webhook_token, string json_payload) + Dictionary values = new(); + RestWebhookExecutePayload pld = new() { - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.SLACK}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); + Content = builder.Content, + Username = builder.Username.HasValue ? builder.Username.Value : null, + AvatarUrl = builder.AvatarUrl.HasValue ? builder.AvatarUrl.Value : null, + IsTTS = builder.IsTTS, + Embeds = builder.Embeds, + Components = builder.Components, + }; - var url = Utilities.GetApiUriBuilderFor(path).AddParameter("wait", "true").Build(); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: json_payload); - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - return ret; + if (builder.Mentions != null) + { + pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); } - internal async Task ExecuteWebhookGithubAsync(ulong webhook_id, string webhook_token, string json_payload) + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) { - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.GITHUB}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); - - var url = Utilities.GetApiUriBuilderFor(path).AddParameter("wait", "true").Build(); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: json_payload); - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - return ret; + values["payload_json"] = DiscordJson.SerializeObject(pld); } - internal async Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id, DiscordWebhookBuilder builder, IEnumerable attachments) + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; + QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"); + url.AddParameter("wait", "true"); + + if (builder.ThreadId.HasValue) { - builder.Validate(true); + url.AddParameter("thread_id", builder.ThreadId.Value.ToString()); + } + + MultipartRestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Post, + Values = values, + Files = builder.Files, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var mentions = builder.Mentions != null ? new DiscordMentions(builder.Mentions, builder.Mentions.Any()) : null; + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - var pld = new RestWebhookMessageEditPayload - { - Content = builder.Content, - Embeds = builder.Embeds, - Mentions = mentions, - Components = builder.Components, - Attachments = attachments - }; + builder.ResetFileStreamPositions(); - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id, webhook_token, message_id }, out var path); + ret.Discord = this._discord!; + return ret; + } - var values = new Dictionary - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; + internal async ValueTask ExecuteWebhookSlackAsync + ( + ulong webhookId, + string webhookToken, + string jsonPayload + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.SLACK}"; + QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.SLACK}"); + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Post, + Payload = jsonPayload, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + return ret; + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoMultipartAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: builder.Files); + internal async ValueTask ExecuteWebhookGithubAsync + ( + ulong webhookId, + string webhookToken, + string jsonPayload + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token{Endpoints.GITHUB}"; + QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}{Endpoints.GITHUB}"); + url.AddParameter("wait", "true"); + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Post, + Payload = jsonPayload, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + return ret; + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + internal async ValueTask EditWebhookMessageAsync + ( + ulong webhookId, + string webhookToken, + ulong messageId, + DiscordWebhookBuilder builder, + IEnumerable? attachments = null + ) + { + builder.Validate(true); - builder.ResetFileStreamPositions(); + DiscordMentions? mentions = builder.Mentions != null ? new DiscordMentions(builder.Mentions, builder.Mentions.Any()) : null; - return ret; - } + RestWebhookMessageEditPayload pld = new() + { + Content = builder.Content, + Embeds = builder.Embeds, + Mentions = mentions, + Components = builder.Components, + Attachments = attachments + }; - internal Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id, DiscordWebhookBuilder builder, IEnumerable attachments) => - this.EditWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), builder, attachments); + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; - internal async Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id) + Dictionary values = new() { - var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id, webhook_token, message_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); - } - internal Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id) => - this.DeleteWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString()); - #endregion + ["payload_json"] = DiscordJson.SerializeObject(pld) + }; - #region Reactions - internal Task CreateReactionAsync(ulong channel_id, ulong message_id, string emoji) + MultipartRestRequest request = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, message_id, emoji }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Patch, + Values = values, + Files = builder.Files, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, ratelimitWaitOverride: this._discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); - } + DiscordMessage ret = this.PrepareMessage(JObject.Parse(res.Response!)); - internal Task DeleteOwnReactionAsync(ulong channel_id, ulong message_id, string emoji) - { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji }, out var path); + builder.ResetFileStreamPositions(); - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this._discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); - } + return ret; + } - internal Task DeleteUserReactionAsync(ulong channel_id, ulong message_id, ulong user_id, string emoji, string reason) - { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + internal async ValueTask DeleteWebhookMessageAsync + ( + ulong webhookId, + string webhookToken, + ulong messageId + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + IsExemptFromGlobalLimit = true + }; + + await this._rest.ExecuteRequestAsync(request); + } + #endregion + + #region Reactions + internal async ValueTask CreateReactionAsync + ( + ulong channelId, + ulong messageId, + string emoji + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/{Endpoints.ME}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put + }; + + await this._rest.ExecuteRequestAsync(request); + } - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji/:user_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji, user_id }, out var path); + internal async ValueTask DeleteOwnReactionAsync + ( + ulong channelId, + ulong messageId, + string emoji + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/{Endpoints.ME}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this._discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); + internal async ValueTask DeleteUserReactionAsync + ( + ulong channelId, + ulong messageId, + ulong userId, + string emoji, + string reason + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task> GetReactionsAsync(ulong channel_id, ulong message_id, string emoji, ulong? after_id = null, int limit = 25) + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/:user_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{userId}"; + + RestRequest request = new() { - var urlparams = new Dictionary(); - if (after_id.HasValue) - urlparams["after"] = after_id.Value.ToString(CultureInfo.InvariantCulture); + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - urlparams["limit"] = limit.ToString(CultureInfo.InvariantCulture); + internal async ValueTask> GetReactionsAsync + ( + ulong channelId, + ulong messageId, + string emoji, + ulong? afterId = null, + int limit = 25 + ) + { + QueryUriBuilder urlparams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}"); + if (afterId.HasValue) + { + urlparams.AddParameter("after", afterId.Value.ToString(CultureInfo.InvariantCulture)); + } - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id, emoji }, out var path); + urlparams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams)); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji"; - var reacters_raw = JsonConvert.DeserializeObject>(res.Response); - var reacters = new List(); - foreach (var xr in reacters_raw) - { - var usr = new DiscordUser(xr) { Discord = this._discord }; - usr = this._discord.UpdateUserCache(usr); + RestRequest request = new() + { + Route = route, + Url = urlparams.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - reacters.Add(usr); - } + IEnumerable usersRaw = JsonConvert.DeserializeObject>(res.Response!)!; + List users = new(); + foreach (TransportUser xr in usersRaw) + { + DiscordUser usr = new(xr) + { + Discord = this._discord! + }; + usr = this._discord!.UpdateUserCache(usr); + + users.Add(usr); + } - return new ReadOnlyCollection(new List(reacters)); + return new ReadOnlyCollection(new List(users)); + } + + internal async ValueTask DeleteAllReactionsAsync + ( + ulong channelId, + ulong messageId, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal Task DeleteAllReactionsAsync(ulong channel_id, ulong message_id, string reason) - { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}"; - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this._discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); - } + internal async ValueTask DeleteReactionsEmojiAsync + ( + ulong channelId, + ulong messageId, + string emoji + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}"; - internal Task DeleteReactionsEmojiAsync(ulong channel_id, ulong message_id, string emoji) + RestRequest request = new() { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this._rest.ExecuteRequestAsync(request); + } + #endregion - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this._discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); - } - #endregion + #region Emoji + internal async ValueTask> GetGuildEmojisAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; - #region Emoji - internal async Task> GetGuildEmojisAsync(ulong guild_id) + RestRequest request = new() { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + IEnumerable emojisRaw = JsonConvert.DeserializeObject>(res.Response!)!; - var emojisRaw = JsonConvert.DeserializeObject>(res.Response); + this._discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + Dictionary users = new(); + List emojis = new(); + foreach (JObject rawEmoji in emojisRaw) + { + DiscordGuildEmoji discordGuildEmoji = rawEmoji.ToDiscordObject(); - this._discord.Guilds.TryGetValue(guild_id, out var gld); - var users = new Dictionary(); - var emojis = new List(); - foreach (var rawEmoji in emojisRaw) + if (guild is not null) { - var xge = rawEmoji.ToDiscordObject(); - xge.Guild = gld; + discordGuildEmoji.Guild = guild; + } - var xtu = rawEmoji["user"]?.ToDiscordObject(); - if (xtu != null) + TransportUser? rawUser = rawEmoji["user"]?.ToDiscordObject(); + if (rawUser != null) + { + if (!users.ContainsKey(rawUser.Id)) { - if (!users.ContainsKey(xtu.Id)) - { - var user = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); - users[user.Id] = user; - } - - xge.User = users[xtu.Id]; + DiscordUser user = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); + users[user.Id] = user; } - emojis.Add(xge); + discordGuildEmoji.User = users[rawUser.Id]; } - return new ReadOnlyCollection(emojis); + emojis.Add(discordGuildEmoji); } - internal async Task GetGuildEmojiAsync(ulong guild_id, ulong emoji_id) - { - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, emoji_id }, out var path); + return new ReadOnlyCollection(emojis); + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + internal async ValueTask GetGuildEmojiAsync + ( + ulong guildId, + ulong emojiId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; - this._discord.Guilds.TryGetValue(guild_id, out var gld); + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var emoji_raw = JObject.Parse(res.Response); - var emoji = emoji_raw.ToDiscordObject(); - emoji.Guild = gld; + this._discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); - var xtu = emoji_raw["user"]?.ToDiscordObject(); - if (xtu != null) - emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); + JObject emojiRaw = JObject.Parse(res.Response!); + DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); - return emoji; + if (guild is not null) + { + emoji.Guild = guild; } - internal async Task CreateGuildEmojiAsync(ulong guild_id, string name, string imageb64, IEnumerable roles, string reason) + TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); + if (rawUser != null) { - var pld = new RestGuildEmojiCreatePayload - { - Name = name, - ImageB64 = imageb64, - Roles = roles?.ToArray() - }; - - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); - - this._discord.Guilds.TryGetValue(guild_id, out var gld); + emoji.User = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); + } - var emoji_raw = JObject.Parse(res.Response); - var emoji = emoji_raw.ToDiscordObject(); - emoji.Guild = gld; + return emoji; + } - var xtu = emoji_raw["user"]?.ToDiscordObject(); - emoji.User = xtu != null - ? gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu) - : this._discord.CurrentUser; + internal async ValueTask CreateGuildEmojiAsync + ( + ulong guildId, + string name, + string imageb64, + IEnumerable? roles = null, + string? reason = null + ) + { + RestGuildEmojiCreatePayload pld = new() + { + Name = name, + ImageB64 = imageb64, + Roles = roles?.ToArray() + }; - return emoji; + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task ModifyGuildEmojiAsync(ulong guild_id, ulong emoji_id, string name, IEnumerable roles, string reason) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; + + RestRequest request = new() { - var pld = new RestGuildEmojiModifyPayload - { - Name = name, - Roles = roles?.ToArray() - }; + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + this._discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, emoji_id }, out var path); + JObject emojiRaw = JObject.Parse(res.Response!); + DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); + if (guild is not null) + { + emoji.Guild = guild; + } - this._discord.Guilds.TryGetValue(guild_id, out var gld); + TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); + emoji.User = rawUser != null + ? guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser) + : this._discord.CurrentUser; - var emoji_raw = JObject.Parse(res.Response); - var emoji = emoji_raw.ToDiscordObject(); - emoji.Guild = gld; + return emoji; + } - var xtu = emoji_raw["user"]?.ToDiscordObject(); - if (xtu != null) - emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); + internal async ValueTask ModifyGuildEmojiAsync + ( + ulong guildId, + ulong emojiId, + string? name = null, + IEnumerable? roles = null, + string? reason = null + ) + { + RestGuildEmojiModifyPayload pld = new() + { + Name = name, + Roles = roles?.ToArray() + }; - return emoji; + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal Task DeleteGuildEmojiAsync(ulong guild_id, ulong emoji_id, string reason) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; + + RestRequest request = new() { - var headers = new Dictionary(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, emoji_id }, out var path); + this._discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); - var url = Utilities.GetApiUriFor(path); - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + JObject emojiRaw = JObject.Parse(res.Response!); + DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); + + if (guild is not null) + { + emoji.Guild = guild; } - #endregion - #region Application Commands - internal async Task> GetGlobalApplicationCommandsAsync(ulong application_id) + TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); + if (rawUser != null) { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id }, out var path); + emoji.User = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + return emoji; + } - var ret = JsonConvert.DeserializeObject>(res.Response); - foreach (var app in ret) - app.Discord = this._discord; - return ret.ToList(); + internal async ValueTask DeleteGuildEmojiAsync + ( + ulong guildId, + ulong emojiId, + string? reason = null + ) + { + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } - internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong application_id, IEnumerable commands) + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; + + RestRequest request = new() { - var pld = new List(); - foreach (var command in commands) - { - pld.Add(new RestApplicationCommandCreatePayload - { - Type = command.Type, - Name = command.Name, - Description = command.Description, - Options = command.Options, - DefaultPermission = command.DefaultPermission, - NameLocalizations = command.NameLocalizations, - DescriptionLocalizations = command.DescriptionLocalizations, - AllowDMUsage = command.AllowDMUsage, - DefaultMemberPermissions = command.DefaultMemberPermissions, - NSFW = command.NSFW - }); - } - - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { application_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); + } + #endregion - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); + #region Application Commands + internal async ValueTask> GetGlobalApplicationCommandsAsync + ( + ulong applicationId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var ret = JsonConvert.DeserializeObject>(res.Response); - foreach (var app in ret) - app.Discord = this._discord; - return ret.ToList(); + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationCommand app in ret) + { + app.Discord = this._discord!; } - internal async Task CreateGlobalApplicationCommandAsync(ulong application_id, DiscordApplicationCommand command) + return ret.ToList(); + } + + internal async ValueTask> BulkOverwriteGlobalApplicationCommandsAsync + ( + ulong applicationId, + IEnumerable commands + ) + { + List pld = new(); + foreach (DiscordApplicationCommand command in commands) { - var pld = new RestApplicationCommandCreatePayload + pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, @@ -3262,119 +5176,198 @@ internal async Task CreateGlobalApplicationCommandAsy AllowDMUsage = command.AllowDMUsage, DefaultMemberPermissions = command.DefaultMemberPermissions, NSFW = command.NSFW - }; - - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { application_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); - - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - - return ret; + }); } - internal async Task GetGlobalApplicationCommandAsync(ulong application_id, ulong command_id) + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id, command_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - return ret; + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationCommand app in ret) + { + app.Discord = this._discord!; } - internal async Task EditGlobalApplicationCommandAsync(ulong application_id, ulong command_id, Optional name, Optional description, Optional> options, Optional defaultPermission, Optional nsfw, IReadOnlyDictionary name_localizations = null, IReadOnlyDictionary description_localizations = null, Optional allowDMUsage = default, Optional defaultMemberPermissions = default) - { - var pld = new RestApplicationCommandEditPayload - { - Name = name, - Description = description, - Options = options, - DefaultPermission = defaultPermission, - NameLocalizations = name_localizations, - DescriptionLocalizations = description_localizations, - AllowDMUsage = allowDMUsage, - DefaultMemberPermissions = defaultMemberPermissions, - NSFW = nsfw, - }; + return ret.ToList(); + } - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { application_id, command_id }, out var path); + internal async ValueTask CreateGlobalApplicationCommandAsync + ( + ulong applicationId, + DiscordApplicationCommand command + ) + { + RestApplicationCommandCreatePayload pld = new() + { + Type = command.Type, + Name = command.Name, + Description = command.Description, + Options = command.Options, + DefaultPermission = command.DefaultPermission, + NameLocalizations = command.NameLocalizations, + DescriptionLocalizations = command.DescriptionLocalizations, + AllowDMUsage = command.AllowDMUsage, + DefaultMemberPermissions = command.DefaultMemberPermissions, + NSFW = command.NSFW + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + + return ret; + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); + internal async ValueTask GetGlobalApplicationCommandAsync + ( + ulong applicationId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + + return ret; + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + internal async ValueTask EditGlobalApplicationCommandAsync + ( + ulong applicationId, + ulong commandId, + Optional name = default, + Optional description = default, + Optional> options = default, + Optional defaultPermission = default, + Optional nsfw = default, + IReadOnlyDictionary? nameLocalizations = null, + IReadOnlyDictionary? descriptionLocalizations = null, + Optional allowDmUsage = default, + Optional defaultMemberPermissions = default + ) + { + RestApplicationCommandEditPayload pld = new() + { + Name = name, + Description = description, + Options = options, + DefaultPermission = defaultPermission, + NameLocalizations = nameLocalizations, + DescriptionLocalizations = descriptionLocalizations, + AllowDMUsage = allowDmUsage, + DefaultMemberPermissions = defaultMemberPermissions, + NSFW = nsfw, + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + + return ret; + } - return ret; - } + internal async ValueTask DeleteGlobalApplicationCommandAsync + ( + ulong applicationId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; - internal async Task DeleteGlobalApplicationCommandAsync(ulong application_id, ulong command_id) + RestRequest request = new() { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id, command_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this._rest.ExecuteRequestAsync(request); + } - var url = Utilities.GetApiUriFor(path); - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); - } + internal async ValueTask> GetGuildApplicationCommandsAsync + ( + ulong applicationId, + ulong guildId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"; - internal async Task> GetGuildApplicationCommandsAsync(ulong application_id, ulong guild_id) + RestRequest request = new() { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - - var ret = JsonConvert.DeserializeObject>(res.Response); - foreach (var app in ret) - app.Discord = this._discord; - return ret.ToList(); - } + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong application_id, ulong guild_id, IEnumerable commands) + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationCommand app in ret) { - var pld = new List(); - foreach (var command in commands) - { - pld.Add(new RestApplicationCommandCreatePayload - { - Type = command.Type, - Name = command.Name, - Description = command.Description, - Options = command.Options, - DefaultPermission = command.DefaultPermission, - NameLocalizations = command.NameLocalizations, - DescriptionLocalizations = command.DescriptionLocalizations, - AllowDMUsage = command.AllowDMUsage, - DefaultMemberPermissions = command.DefaultMemberPermissions, - NSFW = command.NSFW - }); - } - - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); - - var ret = JsonConvert.DeserializeObject>(res.Response); - foreach (var app in ret) - app.Discord = this._discord; - return ret.ToList(); + app.Discord = this._discord!; } - internal async Task CreateGuildApplicationCommandAsync(ulong application_id, ulong guild_id, DiscordApplicationCommand command) + return ret.ToList(); + } + + internal async ValueTask> BulkOverwriteGuildApplicationCommandsAsync + ( + ulong applicationId, + ulong guildId, + IEnumerable commands + ) + { + List pld = new(); + foreach (DiscordApplicationCommand command in commands) { - var pld = new RestApplicationCommandCreatePayload + pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, @@ -3386,521 +5379,913 @@ internal async Task CreateGuildApplicationCommandAsyn AllowDMUsage = command.AllowDMUsage, DefaultMemberPermissions = command.DefaultMemberPermissions, NSFW = command.NSFW - }; - - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { application_id, guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); - - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; - - return ret; + }); } - internal async Task GetGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id) + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationCommand app in ret) { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id, command_id }, out var path); + app.Discord = this._discord!; + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + return ret.ToList(); + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + internal async ValueTask CreateGuildApplicationCommandAsync + ( + ulong applicationId, + ulong guildId, + DiscordApplicationCommand command + ) + { + RestApplicationCommandCreatePayload pld = new() + { + Type = command.Type, + Name = command.Name, + Description = command.Description, + Options = command.Options, + DefaultPermission = command.DefaultPermission, + NameLocalizations = command.NameLocalizations, + DescriptionLocalizations = command.DescriptionLocalizations, + AllowDMUsage = command.AllowDMUsage, + DefaultMemberPermissions = command.DefaultMemberPermissions, + NSFW = command.NSFW + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + + return ret; + } - return ret; - } + internal async ValueTask GetGuildApplicationCommandAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; - internal async Task EditGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id, Optional name, Optional description, Optional> options, Optional defaultPermission, Optional nsfw, IReadOnlyDictionary name_localizations = null, IReadOnlyDictionary description_localizations = null, Optional allowDMUsage = default, Optional defaultMemberPermissions = default) + RestRequest request = new() { - var pld = new RestApplicationCommandEditPayload - { - Name = name, - Description = description, - Options = options, - DefaultPermission = defaultPermission, - NameLocalizations = name_localizations, - DescriptionLocalizations = description_localizations, - AllowDMUsage = allowDMUsage, - DefaultMemberPermissions = defaultMemberPermissions, - NSFW = nsfw - }; + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { application_id, guild_id, command_id }, out var path); + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); + return ret; + } - var ret = JsonConvert.DeserializeObject(res.Response); - ret.Discord = this._discord; + internal async ValueTask EditGuildApplicationCommandAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId, + Optional name = default, + Optional description = default, + Optional> options = default, + Optional defaultPermission = default, + Optional nsfw = default, + IReadOnlyDictionary? nameLocalizations = null, + IReadOnlyDictionary? descriptionLocalizations = null, + Optional allowDmUsage = default, + Optional defaultMemberPermissions = default + ) + { + RestApplicationCommandEditPayload pld = new() + { + Name = name, + Description = description, + Options = options, + DefaultPermission = defaultPermission, + NameLocalizations = nameLocalizations, + DescriptionLocalizations = descriptionLocalizations, + AllowDMUsage = allowDmUsage, + DefaultMemberPermissions = defaultMemberPermissions, + NSFW = nsfw + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; + + return ret; + } - return ret; - } + internal async ValueTask DeleteGuildApplicationCommandAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this._rest.ExecuteRequestAsync(request); + } - internal async Task DeleteGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id) + internal async ValueTask CreateInteractionResponseAsync + ( + ulong interactionId, + string interactionToken, + InteractionResponseType type, + DiscordInteractionResponseBuilder builder + ) + { + if (builder?.Embeds != null) { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id, guild_id, command_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route); + foreach (DiscordEmbed embed in builder.Embeds) + { + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } } - internal async Task CreateInteractionResponseAsync(ulong interaction_id, string interaction_token, InteractionResponseType type, DiscordInteractionResponseBuilder builder) + RestInteractionResponsePayload pld = new() { - if (builder?.Embeds != null) - foreach (var embed in builder.Embeds) - if (embed.Timestamp != null) - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - - var pld = new RestInteractionResponsePayload + Type = type, + Data = builder is not null + ? new DiscordInteractionApplicationCommandCallbackData { - Type = type, - Data = builder != null ? new DiscordInteractionApplicationCommandCallbackData - { - Content = builder.Content, - Title = builder.Title, - CustomId = builder.CustomId, - Embeds = builder.Embeds, - IsTTS = builder.IsTTS, - Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false), - Flags = builder.Flags, - Components = builder.Components, - Choices = builder.Choices - } : null - }; - - var values = new Dictionary(); - - if (builder != null) - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null) - values["payload_json"] = DiscordJson.SerializeObject(pld); - - var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { interaction_id, interaction_token }, out var path); + Content = builder.Content, + Title = builder.Title, + CustomId = builder.CustomId, + Embeds = builder.Embeds, + IsTTS = builder.IsTTS, + Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false), + Flags = builder.Flags, + Components = builder.Components, + Choices = builder.Choices + } + : null + }; - var url = Utilities.GetApiUriBuilderFor(path).AddParameter("wait", "true").Build(); - if (builder != null) - { - await this.DoMultipartAsync(this._discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); + Dictionary values = new(); - builder.ResetFileStreamPositions(); - } - else + if (builder != null) + { + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) { - await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); + values["payload_json"] = DiscordJson.SerializeObject(pld); } } - internal async Task GetOriginalInteractionResponseAsync(ulong application_id, string interaction_token) + string route = $"{Endpoints.INTERACTIONS}/{interactionId}/:interaction_token/{Endpoints.CALLBACK}"; + string url = $"{Endpoints.INTERACTIONS}/{interactionId}/{interactionToken}/{Endpoints.CALLBACK}"; + + if (builder is not null) { - var route = $"{Endpoints.WEBHOOKS}/:application_id/:interaction_token{Endpoints.MESSAGES}{Endpoints.ORIGINAL}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id, interaction_token }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var ret = JsonConvert.DeserializeObject(res.Response); + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Values = values, + Files = builder.Files, + IsExemptFromGlobalLimit = true + }; + + await this._rest.ExecuteRequestAsync(request); - ret.Discord = this._discord; - return ret; + builder.ResetFileStreamPositions(); } + else + { + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + await this._rest.ExecuteRequestAsync(request); + } + } - internal Task EditOriginalInteractionResponseAsync(ulong application_id, string interaction_token, DiscordWebhookBuilder builder, IEnumerable attachments) => - this.EditWebhookMessageAsync(application_id, interaction_token, "@original", builder, attachments); - - internal Task DeleteOriginalInteractionResponseAsync(ulong application_id, string interaction_token) => - this.DeleteWebhookMessageAsync(application_id, interaction_token, "@original"); + internal async ValueTask GetOriginalInteractionResponseAsync + ( + ulong applicationId, + string interactionToken + ) + { + string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/{Endpoints.ORIGINAL}"; + string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/{Endpoints.ORIGINAL}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this._discord!; + return ret; + } - internal async Task CreateFollowupMessageAsync(ulong application_id, string interaction_token, DiscordFollowupMessageBuilder builder) + internal async ValueTask EditOriginalInteractionResponseAsync + ( + ulong applicationId, + string interactionToken, + DiscordWebhookBuilder builder, + IEnumerable attachments + ) + { { - builder.Validate(); + builder.Validate(true); - if (builder.Embeds != null) - foreach (var embed in builder.Embeds) - if (embed.Timestamp != null) - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + DiscordMentions? mentions = builder.Mentions != null ? new DiscordMentions(builder.Mentions, builder.Mentions.Any()) : null; - var values = new Dictionary(); - var pld = new RestFollowupMessageCreatePayload + RestWebhookMessageEditPayload pld = new() { Content = builder.Content, - IsTTS = builder.IsTTS, Embeds = builder.Embeds, - Flags = builder._flags, - Components = builder.Components + Mentions = mentions, + Components = builder.Components, + Attachments = attachments }; - if (builder.Mentions != null) - pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); + string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/@original"; + string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/@original"; - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null) - values["payload_json"] = DiscordJson.SerializeObject(pld); + Dictionary values = new() + { + ["payload_json"] = DiscordJson.SerializeObject(pld) + }; + + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Values = values, + Files = builder.Files, + IsExemptFromGlobalLimit = true + }; - var route = $"{Endpoints.WEBHOOKS}/:application_id/:interaction_token"; - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { application_id, interaction_token }, out var path); + RestResponse res = await this._rest.ExecuteRequestAsync(request); - var url = Utilities.GetApiUriBuilderFor(path).AddParameter("wait", "true").Build(); - var res = await this.DoMultipartAsync(this._discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); - var ret = JsonConvert.DeserializeObject(res.Response); + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this._discord!; - builder.ResetFileStreamPositions(); + foreach (DiscordMessageFile? file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) + { + file.Stream.Position = file.ResetPositionTo!.Value; + } - ret.Discord = this._discord; return ret; } + } - internal Task GetFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id) => - this.GetWebhookMessageAsync(application_id, interaction_token, message_id); - - internal Task EditFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id, DiscordWebhookBuilder builder, IEnumerable attachments) => - this.EditWebhookMessageAsync(application_id, interaction_token, message_id, builder, attachments); - - internal Task DeleteFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id) => - this.DeleteWebhookMessageAsync(application_id, interaction_token, message_id); + internal async ValueTask DeleteOriginalInteractionResponseAsync + ( + ulong applicationId, + string interactionToken + ) + { + string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/@original"; + string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/@original"; - internal async Task> GetGuildApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id) + RestRequest request = new() { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id }, out var path); + Route = route, + Url = url, + Method = HttpMethod.Delete, + IsExemptFromGlobalLimit = true + }; - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var ret = JsonConvert.DeserializeObject>(res.Response); + await this._rest.ExecuteRequestAsync(request); + } - foreach (var perm in ret) - perm.Discord = this._discord; - return ret.ToList(); - } + internal async ValueTask CreateFollowupMessageAsync + ( + ulong applicationId, + string interactionToken, + DiscordFollowupMessageBuilder builder + ) + { + builder.Validate(); - internal async Task GetApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, ulong command_id) + if (builder.Embeds != null) { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id, command_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var ret = JsonConvert.DeserializeObject(res.Response); - - ret.Discord = this._discord; - return ret; + foreach (DiscordEmbed embed in builder.Embeds) + { + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } } - internal async Task EditApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, ulong command_id, IEnumerable permissions) + Dictionary values = new(); + RestFollowupMessageCreatePayload pld = new() { - var pld = new RestEditApplicationCommandPermissionsPayload - { - Permissions = permissions - }; + Content = builder.Content, + IsTTS = builder.IsTTS, + Embeds = builder.Embeds, + Flags = builder._flags, + Components = builder.Components + }; - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id, command_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); - var ret = JsonConvert.DeserializeObject(res.Response); - - ret.Discord = this._discord; - return ret; + if (builder.Mentions != null) + { + pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); } - internal async Task> BatchEditApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, IEnumerable permissions) + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) { - var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id }, out var path); - - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(permissions)); - var ret = JsonConvert.DeserializeObject>(res.Response); - - foreach (var perm in ret) - perm.Discord = this._discord; - return ret.ToList(); + values["payload_json"] = DiscordJson.SerializeObject(pld); } - #endregion - #region Misc - internal Task GetCurrentApplicationInfoAsync() - => this.GetApplicationInfoAsync("@me"); + string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}"; + string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}"; + + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Values = values, + Files = builder.Files, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - internal Task GetApplicationInfoAsync(ulong application_id) - => this.GetApplicationInfoAsync(application_id.ToString(CultureInfo.InvariantCulture)); + builder.ResetFileStreamPositions(); - private async Task GetApplicationInfoAsync(string application_id) - { - var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id }, out var path); + ret.Discord = this._discord!; + return ret; + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + internal ValueTask GetFollowupMessageAsync + ( + ulong applicationId, + string interactionToken, + ulong messageId + ) + => this.GetWebhookMessageAsync(applicationId, interactionToken, messageId); + + internal ValueTask EditFollowupMessageAsync + ( + ulong applicationId, + string interactionToken, + ulong messageId, + DiscordWebhookBuilder builder, + IEnumerable attachments + ) => + this.EditWebhookMessageAsync(applicationId, interactionToken, messageId, builder, attachments); + + internal ValueTask DeleteFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) + => this.DeleteWebhookMessageAsync(applicationId, interactionToken, messageId); + + internal async ValueTask> GetGuildApplicationCommandPermissionsAsync + ( + ulong applicationId, + ulong guildId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; - return JsonConvert.DeserializeObject(res.Response); + foreach (DiscordGuildApplicationCommandPermissions perm in ret) + { + perm.Discord = this._discord!; } - internal async Task> GetApplicationAssetsAsync(DiscordApplication application) - { - var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id{Endpoints.ASSETS}"; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { application_id = application.Id }, out var path); + return ret.ToList(); + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); + internal async ValueTask GetApplicationCommandPermissionsAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id/{Endpoints.PERMISSIONS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}/{Endpoints.PERMISSIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordGuildApplicationCommandPermissions ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this._discord!; + return ret; + } - var assets = JsonConvert.DeserializeObject>(res.Response); - foreach (var asset in assets) - { - asset.Discord = application.Discord; - asset.Application = application; - } + internal async ValueTask EditApplicationCommandPermissionsAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId, + IEnumerable permissions + ) + { + + RestEditApplicationCommandPermissionsPayload pld = new() + { + Permissions = permissions + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id/{Endpoints.PERMISSIONS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}/{Endpoints.PERMISSIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordGuildApplicationCommandPermissions ret = + JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this._discord!; + return ret; + } - return new ReadOnlyCollection(new List(assets)); - } + internal async ValueTask> BatchEditApplicationCommandPermissionsAsync + ( + ulong applicationId, + ulong guildId, + IEnumerable permissions + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(permissions) + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + IEnumerable ret = + JsonConvert.DeserializeObject>(res.Response!)!; - internal async Task GetGatewayInfoAsync() + foreach (DiscordGuildApplicationCommandPermissions perm in ret) { - var headers = Utilities.GetBaseHeaders(); - var route = Endpoints.GATEWAY; - if (this._discord.Configuration.TokenType == TokenType.Bot) - route += Endpoints.BOT; - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); + perm.Discord = this._discord!; + } - var url = Utilities.GetApiUriFor(path); - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route, headers); + return ret.ToList(); + } + #endregion + + #region Misc + internal ValueTask GetCurrentApplicationInfoAsync() + => this.GetApplicationInfoAsync("@me"); + + internal ValueTask GetApplicationInfoAsync + ( + ulong applicationId + ) + => this.GetApplicationInfoAsync(applicationId.ToString(CultureInfo.InvariantCulture)); + + private async ValueTask GetApplicationInfoAsync + ( + string applicationId + ) + { + string route = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/:application_id"; + string url = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/{applicationId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } - var info = JObject.Parse(res.Response).ToDiscordObject(); - info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); - return info; - } - #endregion + internal async ValueTask> GetApplicationAssetsAsync + ( + DiscordApplication application + ) + { + string route = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/:application_id/{Endpoints.ASSETS}"; + string url = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/{application.Id}/{Endpoints.ASSETS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); - public async Task CreateForumPostAsync - ( - ulong channelId, - string name, - AutoArchiveDuration? autoArchiveDuration, - int? rate_limit_per_user, - DiscordMessageBuilder message, - IEnumerable appliedTags - ) + IEnumerable assets = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationAsset asset in assets) { - var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}"; + asset.Discord = application.Discord; + asset.Application = application; + } - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); + return new ReadOnlyCollection(new List(assets)); + } - var url = Utilities.GetApiUriFor(path); + internal async ValueTask GetGatewayInfoAsync() + { + Dictionary headers = new(); + string route = Endpoints.GATEWAY + "/"; + if (this._discord!.Configuration.TokenType == TokenType.Bot) + { + route += Endpoints.BOT; + } + + string url = route; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get, + Headers = headers + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + + GatewayInfo info = JObject.Parse(res.Response!).ToDiscordObject(); + info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); + return info; + } + #endregion + + public async ValueTask CreateForumPostAsync + ( + ulong channelId, + string name, + DiscordMessageBuilder message, + AutoArchiveDuration? autoArchiveDuration = null, + int? rateLimitPerUser = null, + IEnumerable? appliedTags = null + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; - var pld = new RestForumPostCreatePayload + RestForumPostCreatePayload pld = new() + { + Name = name, + ArchiveAfter = autoArchiveDuration, + RateLimitPerUser = rateLimitPerUser, + Message = new RestChannelMessageCreatePayload { - Name = name, - ArchiveAfter = autoArchiveDuration, - RateLimitPerUser = rate_limit_per_user, - Message = new RestChannelMessageCreatePayload - { - Content = message.Content, - HasContent = !string.IsNullOrWhiteSpace(message.Content), - Embeds = message.Embeds, - HasEmbed = message.Embeds.Count > 0, - Mentions = new DiscordMentions(message.Mentions, message.Mentions.Any()), - Components = message.Components, - StickersIds = message.Stickers?.Select(s => s.Id) ?? Array.Empty(), - }, - AppliedTags = appliedTags + Content = message.Content, + HasContent = !string.IsNullOrWhiteSpace(message.Content), + Embeds = message.Embeds, + HasEmbed = message.Embeds.Count > 0, + Mentions = new DiscordMentions(message.Mentions, message.Mentions.Any()), + Components = message.Components, + StickersIds = message.Stickers?.Select(s => s.Id) ?? Array.Empty(), + }, + AppliedTags = appliedTags + }; + + JObject ret; + RestResponse res; + if (message.Files.Count is 0) + { + RestRequest req = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) }; - - JObject ret; - if (message.Files.Count is 0) + + res = await this._rest.ExecuteRequestAsync(req); + ret = JObject.Parse(res.Response!); + } + else + { + Dictionary values = new() { - var res = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); - - ret = JObject.Parse(res.Response); - } - else + ["payload_json"] = DiscordJson.SerializeObject(pld) + }; + + MultipartRestRequest req = new() { - var values = new Dictionary - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; - - var res = await this.DoMultipartAsync(this._discord, bucket, url, RestRequestMethod.POST, route, values: values, files: message.Files); + Route = route, + Url = url, + Method = HttpMethod.Post, + Values = values, + Files = message.Files + }; + + res = await this._rest.ExecuteRequestAsync(req); + ret = JObject.Parse(res.Response!); + } - ret = JObject.Parse(res.Response); - } + JToken? msgToken = ret["message"]; + ret.Remove("message"); - var msgToken = ret["message"]; - ret.Remove("message"); - - var msg = this.PrepareMessage(msgToken); - // We know the return type; deserialize directly. - var chn = ret.ToDiscordObject(); - chn.Discord = this._discord; - - return new DiscordForumPostStarter(chn, msg); - } - - /// - /// Internal method to create an auto-moderation rule in a guild. - /// - /// The id of the guild where the rule will be created. - /// The rule name. - /// The Discord event that will trigger the rule. - /// The rule trigger. - /// The trigger metadata. - /// The actions that will run when a rule is triggered. - /// Whenever the rule is enabled or not. - /// The exempted roles that will not trigger the rule. - /// The exempted channels that will not trigger the rule. - /// The reason for audits logs. - /// The created rule. - internal async Task CreateGuildAutoModerationRuleAsync - ( - ulong guild_id, - string name, - RuleEventType event_type, - RuleTriggerType trigger_type, - DiscordRuleTriggerMetadata trigger_metadata, - IReadOnlyList actions, - Optional enabled = default, - Optional> exempt_roles = default, - Optional> exempt_channels = default, - string reason = null - ) - { - string route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUTO_MODERATION}{Endpoints.RULES}"; - - var bucket = this._rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path); - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - string payload = DiscordJson.SerializeObject(new - { - guild_id, - name, - event_type, - trigger_type, - trigger_metadata, - actions, - enabled, - exempt_roles = exempt_roles.Value.Select(x => x.Id).ToArray(), - exempt_channels = exempt_channels.Value.Select(x => x.Id).ToArray() - }); + DiscordMessage msg = this.PrepareMessage(msgToken!); + // We know the return type; deserialize directly. + DiscordThreadChannel chn = ret.ToDiscordObject(); - var req = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.POST, route, headers, payload); - var rule = JsonConvert.DeserializeObject(req.Response); - - return rule; - } - - /// - /// Internal method to get an auto-moderation rule in a guild. - /// - /// The guild id where the rule is in. - /// The rule id. - /// The rule found. - internal async Task GetGuildAutoModerationRuleAsync(ulong guild_id, ulong rule_id) - { - string route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUTO_MODERATION}{Endpoints.RULES}/:rule_id"; - - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, rule_id }, out var path); - var url = Utilities.GetApiUriFor(path); - var req = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var rule = JsonConvert.DeserializeObject(req.Response); - - return rule; - } - - /// - /// Internal method to get all auto-moderation rules in a guild. - /// - /// The guild id where rules are in. - /// The rules found. - internal async Task> GetGuildAutoModerationRulesAsync(ulong guild_id) - { - string route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUTO_MODERATION}{Endpoints.RULES}"; - - var bucket = this._rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); - var url = Utilities.GetApiUriFor(path); - var req = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.GET, route); - var rules = JsonConvert.DeserializeObject>(req.Response); - - return rules; - } - - /// - /// Internal method to modify an auto-moderation rule in a guild. - /// - /// The id of the guild where the rule will be modified. - /// The rule name. - /// The Discord event that will trigger the rule. - /// The trigger metadata. - /// The actions that will run when a rule is triggered. - /// Whenever the rule is enabled or not. - /// The exempted roles that will not trigger the rule. - /// The exempted channels that will not trigger the rule. - /// The reason for audits logs. - /// The modified rule. - internal async Task ModifyGuildAutoModerationRuleAsync - ( - ulong guild_id, - ulong rule_id, - Optional name, - Optional event_type, - Optional trigger_metadata, - Optional> actions, - Optional enabled, - Optional> exempt_roles, - Optional> exempt_channels, - string reason = null - ) - { - string route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUTO_MODERATION}{Endpoints.RULES}/:rule_id"; - - var bucket = this._rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, rule_id }, out var path); - var url = Utilities.GetApiUriFor(path); - - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; - - string payload = DiscordJson.SerializeObject(new - { - name, - event_type, - trigger_metadata, - actions, - enabled, - exempt_roles = exempt_roles.Value.Select(x => x.Id).ToArray(), - exempt_channels = exempt_channels.Value.Select(x => x.Id).ToArray() - }); + return new DiscordForumPostStarter(chn, msg); + } - var req = await this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload); - var rule = JsonConvert.DeserializeObject(req.Response); + /// + /// Internal method to create an auto-moderation rule in a guild. + /// + /// The id of the guild where the rule will be created. + /// The rule name. + /// The Discord event that will trigger the rule. + /// The rule trigger. + /// The trigger metadata. + /// The actions that will run when a rule is triggered. + /// Whenever the rule is enabled or not. + /// The exempted roles that will not trigger the rule. + /// The exempted channels that will not trigger the rule. + /// The reason for audits logs. + /// The created rule. + internal async ValueTask CreateGuildAutoModerationRuleAsync + ( + ulong guildId, + string name, + RuleEventType eventType, + RuleTriggerType triggerType, + DiscordRuleTriggerMetadata triggerMetadata, + IReadOnlyList actions, + Optional enabled = default, + Optional> exemptRoles = default, + Optional> exemptChannels = default, + string? reason = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; + + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string payload = DiscordJson.SerializeObject(new + { + guild_id = guildId, + name, + event_type = eventType, + trigger_type = triggerType, + trigger_metadata = triggerMetadata, + actions, + enabled, + exempt_roles = exemptRoles.Value.Select(x => x.Id).ToArray(), + exempt_channels = exemptChannels.Value.Select(x => x.Id).ToArray() + }); + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Headers = headers, + Payload = payload + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; + + return rule; + } - return rule; - } + /// + /// Internal method to get an auto-moderation rule in a guild. + /// + /// The guild id where the rule is in. + /// The rule id. + /// The rule found. + internal async ValueTask GetGuildAutoModerationRuleAsync + ( + ulong guildId, + ulong ruleId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; + + return rule; + } - /// - /// Internal method to delete an auto-moderation rule in a guild. - /// - /// The id of the guild where the rule is in. - /// The rule id that will be deleted. - /// The reason for audits logs. - internal Task DeleteGuildAutoModerationRuleAsync(ulong guild_id, ulong rule_id, string reason) - { - string route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUTO_MODERATION}{Endpoints.RULES}/:rule_id"; + /// + /// Internal method to get all auto-moderation rules in a guild. + /// + /// The guild id where rules are in. + /// The rules found. + internal async ValueTask> GetGuildAutoModerationRulesAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + IReadOnlyList rules = JsonConvert.DeserializeObject>(res.Response!)!; + + return rules; + } - var bucket = this._rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, rule_id }, out var path); - var url = Utilities.GetApiUriFor(path); + /// + /// Internal method to modify an auto-moderation rule in a guild. + /// + /// The id of the guild where the rule will be modified. + /// The id of the rule that will be modified. + /// The rule name. + /// The Discord event that will trigger the rule. + /// The trigger metadata. + /// The actions that will run when a rule is triggered. + /// Whenever the rule is enabled or not. + /// The exempted roles that will not trigger the rule. + /// The exempted channels that will not trigger the rule. + /// The reason for audits logs. + /// The modified rule. + internal async ValueTask ModifyGuildAutoModerationRuleAsync + ( + ulong guildId, + ulong ruleId, + Optional name, + Optional eventType, + Optional triggerMetadata, + Optional> actions, + Optional enabled, + Optional> exemptRoles, + Optional> exemptChannels, + string? reason = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; + + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string payload = DiscordJson.SerializeObject(new + { + name, + event_type = eventType, + trigger_metadata = triggerMetadata, + actions, + enabled, + exempt_roles = exemptRoles.Value.Select(x => x.Id).ToArray(), + exempt_channels = exemptChannels.Value.Select(x => x.Id).ToArray() + }); + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Headers = headers, + Payload = payload + }; + + RestResponse res = await this._rest.ExecuteRequestAsync(request); + DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; + + return rule; + } - var headers = Utilities.GetBaseHeaders(); - if (!string.IsNullOrWhiteSpace(reason)) - headers[REASON_HEADER_NAME] = reason; + /// + /// Internal method to delete an auto-moderation rule in a guild. + /// + /// The id of the guild where the rule is in. + /// The rule id that will be deleted. + /// The reason for audits logs. + internal async ValueTask DeleteGuildAutoModerationRuleAsync + ( + ulong guildId, + ulong ruleId, + string? reason = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; - return this.DoRequestAsync(this._discord, bucket, url, RestRequestMethod.DELETE, route, headers); + Dictionary headers = new(); + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; } + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this._rest.ExecuteRequestAsync(request); } } diff --git a/DSharpPlus/Net/Rest/Endpoints.cs b/DSharpPlus/Net/Rest/Endpoints.cs index 9f68349931..4f6e99f5c5 100644 --- a/DSharpPlus/Net/Rest/Endpoints.cs +++ b/DSharpPlus/Net/Rest/Endpoints.cs @@ -1,73 +1,72 @@ -namespace DSharpPlus.Net +namespace DSharpPlus.Net; + +internal static class Endpoints { - internal static class Endpoints - { - public const string API_VERSION = "10"; - public const string BASE_URI = "https://discord.com/api/v" + API_VERSION; + public const string API_VERSION = "10"; + public const string BASE_URI = "https://discord.com/api/v" + API_VERSION; - public const string ORIGINAL = "/@original"; - public const string OAUTH2 = "/oauth2"; - public const string APPLICATIONS = "/applications"; - public const string REACTIONS = "/reactions"; - public const string ME = "/@me"; - public const string PERMISSIONS = "/permissions"; - public const string RECIPIENTS = "/recipients"; - public const string BULK_DELETE = "/bulk-delete"; - public const string INTEGRATIONS = "/integrations"; - public const string SYNC = "/sync"; - public const string PRUNE = "/prune"; - public const string REGIONS = "/regions"; - public const string CONNECTIONS = "/connections"; - public const string ICONS = "/icons"; - public const string GATEWAY = "/gateway"; - public const string AUTH = "/auth"; - public const string LOGIN = "/login"; - public const string CHANNELS = "/channels"; - public const string THREADS = "/threads"; - public const string EVENTS = "/scheduled-events"; - public const string THREAD_MEMBERS = "/thread-members"; - public const string ACTIVE = "/active "; - public const string ARCHIVED = "/archived"; - public const string PUBLIC = "/public"; - public const string PRIVATE = "/private"; - public const string MESSAGES = "/messages"; - public const string PINS = "/pins"; - public const string USERS = "/users"; - public const string GUILDS = "/guilds"; - public const string SEARCH = "/search"; - public const string INVITES = "/invites"; - public const string ROLES = "/roles"; - public const string MEMBERS = "/members"; - public const string TYPING = "/typing"; - public const string AVATARS = "/avatars"; - public const string BANS = "/bans"; - public const string WEBHOOKS = "/webhooks"; - public const string SLACK = "/slack"; - public const string GITHUB = "/github"; - public const string BOT = "/bot"; - public const string VOICE = "/voice"; - public const string AUDIT_LOGS = "/audit-logs"; - public const string ACK = "/ack"; - public const string ASSETS = "/assets"; - public const string EMOJIS = "/emojis"; - public const string VANITY_URL = "/vanity-url"; - public const string WIDGET_PNG = "/widget.png"; - public const string PREVIEW = "/preview"; - public const string FOLLOWERS = "/followers"; - public const string CROSSPOST = "/crosspost"; - public const string WIDGET = "/widget"; - public const string WIDGET_JSON = "/widget.json"; - public const string TEMPLATES = "/templates"; - public const string MEMBER_VERIFICATION = "/member-verification"; - public const string COMMANDS = "/commands"; - public const string INTERACTIONS = "/interactions"; - public const string CALLBACK = "/callback"; - public const string WELCOME_SCREEN = "/welcome-screen"; - public const string VOICE_STATES = "/voice-states"; - public const string STICKERS = "/stickers"; - public const string STICKERPACKS = "/sticker-packs"; - public const string STAGE_INSTANCES = "/stage-instances"; - public const string AUTO_MODERATION = "/auto-moderation"; - public const string RULES = "/rules"; - } + public const string ACK = "ack"; + public const string ACTIVE = "active "; + public const string APPLICATIONS = "applications"; + public const string ARCHIVED = "archived"; + public const string ASSETS = "assets"; + public const string AUDIT_LOGS = "audit-logs"; + public const string AUTH = "auth"; + public const string AUTO_MODERATION = "auto-moderation"; + public const string AVATARS = "avatars"; + public const string BANS = "bans"; + public const string BOT = "bot"; + public const string BULK_DELETE = "bulk-delete"; + public const string CALLBACK = "callback"; + public const string CHANNELS = "channels"; + public const string COMMANDS = "commands"; + public const string CONNECTIONS = "connections"; + public const string CROSSPOST = "crosspost"; + public const string EMOJIS = "emojis"; + public const string EVENTS = "scheduled-events"; + public const string FOLLOWERS = "followers"; + public const string GATEWAY = "gateway"; + public const string GITHUB = "github"; + public const string GUILDS = "guilds"; + public const string ICONS = "icons"; + public const string INTEGRATIONS = "integrations"; + public const string INTERACTIONS = "interactions"; + public const string INVITES = "invites"; + public const string LOGIN = "login"; + public const string ME = "@me"; + public const string MEMBERS = "members"; + public const string MEMBER_VERIFICATION = "member-verification"; + public const string MESSAGES = "messages"; + public const string OAUTH2 = "oauth2"; + public const string ORIGINAL = "@original"; + public const string PERMISSIONS = "permissions"; + public const string PINS = "pins"; + public const string PREVIEW = "preview"; + public const string PRIVATE = "private"; + public const string PRUNE = "prune"; + public const string PUBLIC = "public"; + public const string REACTIONS = "reactions"; + public const string RECIPIENTS = "recipients"; + public const string REGIONS = "regions"; + public const string ROLES = "roles"; + public const string RULES = "rules"; + public const string SEARCH = "search"; + public const string SLACK = "slack"; + public const string STAGE_INSTANCES = "stage-instances"; + public const string STICKERPACKS = "sticker-packs"; + public const string STICKERS = "stickers"; + public const string SYNC = "sync"; + public const string TEMPLATES = "templates"; + public const string THREADS = "threads"; + public const string THREAD_MEMBERS = "thread-members"; + public const string TYPING = "typing"; + public const string USERS = "users"; + public const string VANITY_URL = "vanity-url"; + public const string VOICE = "voice"; + public const string VOICE_STATES = "voice-states"; + public const string WEBHOOKS = "webhooks"; + public const string WELCOME_SCREEN = "welcome-screen"; + public const string WIDGET = "widget"; + public const string WIDGET_JSON = "widget.json"; + public const string WIDGET_PNG = "widget.png"; } diff --git a/DSharpPlus/Net/Rest/IRestRequest.cs b/DSharpPlus/Net/Rest/IRestRequest.cs new file mode 100644 index 0000000000..ca291b4362 --- /dev/null +++ b/DSharpPlus/Net/Rest/IRestRequest.cs @@ -0,0 +1,30 @@ +using System.Net.Http; + +namespace DSharpPlus.Net; + +/// +/// Serves as a generic constraint for the rest client. +/// +internal interface IRestRequest +{ + /// + /// Builds the current rest request object into a request message. + /// + public HttpRequestMessage Build(); + + /// + /// The URL this request is made to. This is distinct from the in that the route + /// cannot contain query parameters or secondary IDs necessary for the request. + /// + public string Url { get; init; } + + /// + /// The ratelimiting route this request is made to. + /// + public string Route { get; init; } + + /// + /// Specifies whether this request is exempt from the global limit. Generally applies to webhook requests. + /// + public bool IsExemptFromGlobalLimit { get; init; } +} diff --git a/DSharpPlus/Net/Rest/MultipartRestRequest.cs b/DSharpPlus/Net/Rest/MultipartRestRequest.cs new file mode 100644 index 0000000000..4137e37bdc --- /dev/null +++ b/DSharpPlus/Net/Rest/MultipartRestRequest.cs @@ -0,0 +1,132 @@ +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2023 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Net; + +/// +/// Represents a multipart HTTP request. +/// +internal readonly record struct MultipartRestRequest : IRestRequest +{ + /// + public string Url { get; init; } + + /// + /// The method for this request. + /// + public HttpMethod Method { get; init; } + + /// + public string Route { get; init; } + + /// + public bool IsExemptFromGlobalLimit { get; init; } + + /// + /// The headers for this request. + /// + public IReadOnlyDictionary? Headers { get; init; } + + /// + /// Gets the dictionary of values attached to this request. + /// + public IReadOnlyDictionary Values { get; init; } + + /// + /// Gets the dictionary of files attached to this request. + /// + public IReadOnlyList Files { get; init; } + + public HttpRequestMessage Build() + { + HttpRequestMessage request = new() + { + Method = this.Method, + RequestUri = new($"{Endpoints.BASE_URI}/{this.Url}") + }; + + if (this.Headers is not null) + { + foreach (KeyValuePair header in this.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + } + + request.Headers.Add("Connection", "keep-alive"); + request.Headers.Add("Keep-Alive", "600"); + + string boundary = "---------------------------" + DateTimeOffset.UtcNow.Ticks.ToString("x"); + + MultipartFormDataContent content = new(boundary); + + if(this.Values is not null) + { + foreach(KeyValuePair element in this.Values) + { + content.Add(new StringContent(element.Value), element.Key); + } + } + + if(this.Files is not null) + { + for(int i = 0; i < this.Files.Count; i++) + { + DiscordMessageFile current = this.Files[i]; + + StreamContent file = new(current.Stream); + + if(current.ContentType is not null) + { + file.Headers.ContentType = MediaTypeHeaderValue.Parse(current.ContentType); + } + + string filename = current.FileType is null + ? current.FileName + : $"{current.FileName}.{current.FileType}"; + + // do we actually need this distinction? it's been made since the beginning of time, + // but it doesn't seem very necessary + if(this.Files.Count > 1) + { + content.Add(file, $"file{i + 1}", filename); + } + else + { + content.Add(file, "file", filename); + } + } + } + + request.Content = content; + + return request; + } +} diff --git a/DSharpPlus/Net/Rest/MultipartWebRequest.cs b/DSharpPlus/Net/Rest/MultipartWebRequest.cs deleted file mode 100644 index e434539015..0000000000 --- a/DSharpPlus/Net/Rest/MultipartWebRequest.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net -{ - /// - /// Represents a multipart HTTP request. - /// - internal sealed class MultipartWebRequest : BaseRestRequest - { - /// - /// Gets the dictionary of values attached to this request. - /// - public IReadOnlyDictionary Values { get; } - - /// - /// Gets the dictionary of files attached to this request. - /// - public IReadOnlyCollection Files { get; } - - internal bool _removeFileCount; - - internal MultipartWebRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null, - IReadOnlyCollection files = null, double? ratelimit_wait_override = null, bool removeFileCount = false) - : base(client, bucket, url, method, route, headers, ratelimit_wait_override) - { - this.Values = values; - this.Files = files; - this._removeFileCount = removeFileCount; - } - } -} diff --git a/DSharpPlus/Net/Rest/RateLimitBucket.cs b/DSharpPlus/Net/Rest/RateLimitBucket.cs index 4a366f8a98..7a0fa91a5c 100644 --- a/DSharpPlus/Net/Rest/RateLimitBucket.cs +++ b/DSharpPlus/Net/Rest/RateLimitBucket.cs @@ -1,237 +1,121 @@ using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.Net +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http.Headers; + +namespace DSharpPlus.Net; + +/// +/// Represents a rate limit bucket. +/// + +// this is 16 bytes, which ensures that we can always move this within at most six CPU cycles on any +// modern xarch CPU, and eight? cycles on modern aarch CPUs +// do not change the order of properties without first verifying that `sizeof(RateLimitBucket)` remains +// 16. note to testers: this requires an unsafe context for... some reason. +// it is painful to use DateTime over DateTimeOffset here, but DateTimeOffset means double the copy cost. +internal record struct RateLimitBucket { /// - /// Represents a rate limit bucket. + /// Gets the number of uses left before pre-emptive rate limit is triggered. /// - internal class RateLimitBucket : IEquatable - { - /// - /// Gets the Id of the guild bucket. - /// - public string GuildId { get; internal set; } - - /// - /// Gets the Id of the channel bucket. - /// - public string ChannelId { get; internal set; } - - /// - /// Gets the ID of the webhook bucket. - /// - public string WebhookId { get; internal set; } - - /// - /// Gets the Id of the ratelimit bucket. - /// - public volatile string BucketId; - - /// - /// Gets or sets the ratelimit hash of this bucket. - /// - public string Hash - { - get => Volatile.Read(ref this._hash); - - internal set - { - this._isUnlimited = value.Contains(UNLIMITED_HASH); + public int Remaining + => this._remaining; - if (this.BucketId != null && !this.BucketId.StartsWith(value)) - { - var id = GenerateBucketId(value, this.GuildId, this.ChannelId, this.WebhookId); - this.BucketId = id; - this.RouteHashes.Add(id); - } - - Volatile.Write(ref this._hash, value); - } - } - - internal string _hash; - - /// - /// Gets the past route hashes associated with this bucket. - /// - public ConcurrentBag RouteHashes { get; } - - /// - /// Gets when this bucket was last called in a request. - /// - public DateTimeOffset LastAttemptAt { get; internal set; } - - /// - /// Gets the number of uses left before pre-emptive rate limit is triggered. - /// - public int Remaining - => this._remaining; - - /// - /// Gets the maximum number of uses within a single bucket. - /// - public int Maximum { get; set; } - - /// - /// Gets the timestamp at which the rate limit resets. - /// - public DateTimeOffset Reset { get; internal set; } - - /// - /// Gets the time interval to wait before the rate limit resets. - /// - public TimeSpan? ResetAfter { get; internal set; } = null; - - internal DateTimeOffset ResetAfterOffset { get; set; } - - internal volatile int _remaining; - - /// - /// Gets whether this bucket has it's ratelimit determined. - /// This will be if the ratelimit is determined. - /// - internal volatile bool _isUnlimited; - - /// - /// If the initial request for this bucket that is determining the rate limits is currently executing - /// This is a int because booleans can't be accessed atomically - /// 0 => False, all other values => True - /// - internal volatile int _limitTesting; - - /// - /// Task to wait for the rate limit test to finish - /// - internal volatile Task _limitTestFinished; - - /// - /// If the rate limits have been determined - /// - internal volatile bool _limitValid; - - /// - /// Rate limit reset in ticks, UTC on the next response after the rate limit has been reset - /// - internal long _nextReset; - - /// - /// If the rate limit is currently being reset. - /// This is a int because booleans can't be accessed atomically. - /// 0 => False, all other values => True - /// - internal volatile int _limitResetting; - - private const string UNLIMITED_HASH = "unlimited"; - - internal RateLimitBucket(string hash, string guild_id, string channel_id, string webhook_id) - { - this.Hash = hash; - this.ChannelId = channel_id; - this.GuildId = guild_id; - this.WebhookId = webhook_id; + /// + /// Gets the timestamp at which the rate limit resets. + /// + public DateTime Reset { get; internal set; } - this.BucketId = GenerateBucketId(hash, guild_id, channel_id, webhook_id); - this.RouteHashes = new ConcurrentBag(); - } + /// + /// Gets the maximum number of uses within a single bucket. + /// + public int Maximum { get; set; } - /// - /// Generates an ID for this request bucket. - /// - /// Hash for this bucket. - /// Guild Id for this bucket. - /// Channel Id for this bucket. - /// Webhook Id for this bucket. - /// Bucket Id. - public static string GenerateBucketId(string hash, string guild_id, string channel_id, string webhook_id) - => $"{hash}:{guild_id}:{channel_id}:{webhook_id}"; - - public static string GenerateHashKey(RestRequestMethod method, string route) - => $"{method}:{route}"; - - public static string GenerateUnlimitedHash(RestRequestMethod method, string route) - => $"{GenerateHashKey(method, route)}:{UNLIMITED_HASH}"; - - /// - /// Returns a string representation of this bucket. - /// - /// String representation of this bucket. - public override string ToString() - { - var guildId = this.GuildId != string.Empty ? this.GuildId : "guild_id"; - var channelId = this.ChannelId != string.Empty ? this.ChannelId : "channel_id"; - var webhookId = this.WebhookId != string.Empty ? this.WebhookId : "webhook_id"; + internal volatile int _remaining; - return $"rate limit bucket [{this.Hash}:{guildId}:{channelId}:{webhookId}] [{this.Remaining}/{this.Maximum}] {(this.ResetAfter.HasValue ? this.ResetAfterOffset : this.Reset)}"; - } + public RateLimitBucket + ( + int maximum, + int remaining, + DateTime reset + ) + { + this.Maximum = maximum; + this._remaining = remaining; + this.Reset = reset; + } - /// - /// Checks whether this is equal to another object. - /// - /// Object to compare to. - /// Whether the object is equal to this . - public override bool Equals(object obj) - => this.Equals(obj as RateLimitBucket); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(RateLimitBucket e) + /// + /// Resets the bucket to the next reset time. + /// + internal void ResetLimit(DateTime nextReset) + { + if(nextReset < this.Reset) { - if (e is null) - return false; - - return ReferenceEquals(this, e) ? true : this.BucketId == e.BucketId; + throw new ArgumentOutOfRangeException + ( + nameof(nextReset), + "The next ratelimit expiration must follow the present expiration." + ); } - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() - => this.BucketId.GetHashCode(); - - /// - /// Sets remaining number of requests to the maximum when the ratelimit is reset - /// - /// - internal async Task TryResetLimitAsync(DateTimeOffset now) - { - if (this.ResetAfter.HasValue) - this.ResetAfter = this.ResetAfterOffset - now; + this._remaining = this.Maximum; + this.Reset = nextReset; + } - if (this._nextReset == 0) - return; + public static bool TryExtractRateLimitBucket + ( + HttpResponseHeaders headers, - if (this._nextReset > now.UtcTicks) - return; + [NotNullWhen(true)] + out RateLimitBucket? bucket + ) + { + bucket = null!; - while (Interlocked.CompareExchange(ref this._limitResetting, 1, 0) != 0) -#pragma warning restore 420 - await Task.Yield(); + try + { + if + ( + !headers.TryGetValues("X-RateLimit-Limit", out IEnumerable? limitRaw) + || !headers.TryGetValues("X-RateLimit-Remaining", out IEnumerable? remainingRaw) + || !headers.TryGetValues("X-RateLimit-Reset", out IEnumerable? ratelimitResetRaw) + ) + { + return false; + } - if (this._nextReset != 0) + if + ( + !int.TryParse(limitRaw.SingleOrDefault(), out int limit) + || !int.TryParse(remainingRaw.SingleOrDefault(), out int remaining) + || !double.TryParse(ratelimitResetRaw.SingleOrDefault(), out double ratelimitReset) + ) { - this._remaining = this.Maximum; - this._nextReset = 0; + return false; } - this._limitResetting = 0; - } + DateTime reset = (DateTimeOffset.UnixEpoch + TimeSpan.FromSeconds(ratelimitReset)).UtcDateTime; - internal void SetInitialValues(int max, int usesLeft, DateTimeOffset newReset) + bucket = new(limit, remaining, reset); + return true; + } + catch { - this.Maximum = max; - this._remaining = usesLeft; - this._nextReset = newReset.UtcTicks; + return false; + } + } - this._limitValid = true; - this._limitTestFinished = null; - this._limitTesting = 0; + internal bool CheckNextRequest() + { + if (this.Remaining <= 0) + { + return this.Reset < DateTime.UtcNow; } + + this._remaining--; + return true; } } diff --git a/DSharpPlus/Net/Rest/RateLimitPolicy.cs b/DSharpPlus/Net/Rest/RateLimitPolicy.cs new file mode 100644 index 0000000000..621dcb87f5 --- /dev/null +++ b/DSharpPlus/Net/Rest/RateLimitPolicy.cs @@ -0,0 +1,126 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +using Polly; + +namespace DSharpPlus.Net; + +internal class RateLimitPolicy : AsyncPolicy +{ + private readonly RateLimitBucket globalBucket; + private readonly MemoryCache cache; + private readonly ILogger logger; + private static readonly TimeSpan second = TimeSpan.FromSeconds(1); + + public RateLimitPolicy(ILogger logger) + { + this.globalBucket = new(50, 50, DateTime.UtcNow.AddSeconds(1)); + + // we don't actually care about any settings on this cache + // in a future day and age, we'll hopefully replace this with an application-global cache, but oh well + this.cache = new + ( + new MemoryCacheOptions + { + } + ); + + this.logger = logger; + } + + protected override async Task ImplementationAsync + ( + Func> action, + Context context, + CancellationToken cancellationToken, + // since the library doesn't use CA(false) at all (#1533), we will proceed to ignore this + bool continueOnCapturedContext = true + ) + { + // fail-fast if we dont have a route to ratelimit to + if(!context.TryGetValue("route", out object rawRoute) || rawRoute is not string route) + { + throw new InvalidOperationException("No route passed. This should be reported to library developers."); + } + + // get global limit + bool exemptFromGlobalLimit = false; + + if(context.TryGetValue("exempt-from-global-limit", out object rawExempt) && rawExempt is bool exempt) + { + exemptFromGlobalLimit = exempt; + } + + // check against ratelimits now + DateTime instant = DateTime.UtcNow; + + if (!exemptFromGlobalLimit) + { + if (this.globalBucket.Reset < instant) + { + this.globalBucket.ResetLimit(instant + second); + } + + if (!this.globalBucket.CheckNextRequest()) + { + HttpResponseMessage synthesizedResponse = new(HttpStatusCode.TooManyRequests); + + synthesizedResponse.Headers.RetryAfter = new RetryConditionHeaderValue(this.globalBucket.Reset - instant); + synthesizedResponse.Headers.Add("DSharpPlus-Internal-Response", "global"); + + this.logger.LogWarning + ( + LoggerEvents.RatelimitPreemptive, + "Pre-emptive ratelimit triggered - waiting until {reset:yyyy-MM-dd HH:mm:ss zzz}.", + this.globalBucket.Reset + ); + + return synthesizedResponse; + } + } + + RateLimitBucket? bucket = this.cache.Get(route); + + if(bucket is not null) + { + if (!bucket.Value.CheckNextRequest()) + { + HttpResponseMessage synthesizedResponse = new(HttpStatusCode.TooManyRequests); + + synthesizedResponse.Headers.RetryAfter = new RetryConditionHeaderValue(bucket.Value.Reset - instant); + synthesizedResponse.Headers.Add("DSharpPlus-Internal-Response", "bucket"); + + this.logger.LogWarning + ( + LoggerEvents.RatelimitPreemptive, + "Pre-emptive ratelimit triggered - waiting until {reset:yyyy-MM-dd HH:mm:ss zzz}.", + bucket.Value.Reset + ); + + return synthesizedResponse; + } + } + + // make the actual request + + HttpResponseMessage response = await action(context, cancellationToken); + + if(!RateLimitBucket.TryExtractRateLimitBucket(response.Headers, out RateLimitBucket? extracted)) + { + return response; + } + + this.cache.CreateEntry(route) + .SetValue(extracted.Value) + .Dispose(); + + return response; + } +} diff --git a/DSharpPlus/Net/Rest/RestClient.cs b/DSharpPlus/Net/Rest/RestClient.cs index e4f0269785..2af3d9ed11 100644 --- a/DSharpPlus/Net/Rest/RestClient.cs +++ b/DSharpPlus/Net/Rest/RestClient.cs @@ -1,690 +1,203 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Reflection; -using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; + using DSharpPlus.Exceptions; + using Microsoft.Extensions.Logging; -namespace DSharpPlus.Net -{ - /// - /// Represents a client used to make REST requests. - /// - internal sealed class RestClient : IDisposable - { - private static Regex RouteArgumentRegex { get; } = new Regex(@":([a-z_]+)"); - private HttpClient HttpClient { get; } - private BaseDiscordClient Discord { get; } - private ILogger Logger { get; } - private ConcurrentDictionary RoutesToHashes { get; } - private ConcurrentDictionary HashesToBuckets { get; } - private ConcurrentDictionary RequestQueue { get; } - private AsyncManualResetEvent GlobalRateLimitEvent { get; } - private bool UseResetAfter { get; } - - private CancellationTokenSource _bucketCleanerTokenSource; - private TimeSpan _bucketCleanupDelay = TimeSpan.FromSeconds(60); - private volatile bool _cleanerRunning; - private Task _cleanerTask; - private volatile bool _disposed; - - internal RestClient(BaseDiscordClient client) - : this(client.Configuration.Proxy, client.Configuration.HttpTimeout, client.Configuration.UseRelativeRatelimit, client.Logger) - { - this.Discord = client; - this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client)); - } +using Polly; +using Polly.Retry; +using Polly.Wrap; - internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit, - ILogger logger) // This is for meta-clients, such as the webhook client - { - this.Logger = logger; +namespace DSharpPlus.Net; - var httphandler = new HttpClientHandler - { - UseCookies = false, - AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, - UseProxy = proxy != null, - Proxy = proxy - }; +/// +/// Represents a client used to make REST requests. +/// +internal sealed partial class RestClient : IDisposable +{ - this.HttpClient = new HttpClient(httphandler) - { - BaseAddress = new Uri(Utilities.GetApiBaseUri()), - Timeout = timeout - }; + [GeneratedRegex(":([a-z_]+)")] + private static partial Regex GenerateRouteArgumentRegex(); - this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); + private static Regex RouteArgumentRegex { get; } = GenerateRouteArgumentRegex(); + private HttpClient HttpClient { get; } + private BaseDiscordClient? Discord { get; } + private ILogger Logger { get; } + private AsyncManualResetEvent GlobalRateLimitEvent { get; } - this.RoutesToHashes = new ConcurrentDictionary(); - this.HashesToBuckets = new ConcurrentDictionary(); - this.RequestQueue = new ConcurrentDictionary(); + private AsyncPolicyWrap RateLimitPolicy { get; } - this.GlobalRateLimitEvent = new AsyncManualResetEvent(true); - this.UseResetAfter = useRelativeRatelimit; - } + private volatile bool _disposed; - public RateLimitBucket GetBucket(RestRequestMethod method, string route, object route_params, out string url) - { - var rparams_props = route_params.GetType() - .GetTypeInfo() - .DeclaredProperties; - var rparams = new Dictionary(); - foreach (var xp in rparams_props) - { - var val = xp.GetValue(route_params); - if (val is string xs) - rparams[xp.Name] = xs; - else if (val is DateTime dt) - rparams[xp.Name] = dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); - else if (val is DateTimeOffset dto) - rparams[xp.Name] = dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); - else rparams[xp.Name] = val is IFormattable xf ? xf.ToString(null, CultureInfo.InvariantCulture) : val.ToString(); - } - - var guild_id = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : ""; - var channel_id = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : ""; - var webhook_id = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : ""; - - // Create a generic route (minus major params) key - // ex: POST:/channels/channel_id/messages - var hashKey = RateLimitBucket.GenerateHashKey(method, route); - - // We check if the hash is present, using our generic route (without major params) - // ex: in POST:/channels/channel_id/messages, out 80c17d2f203122d936070c88c8d10f33 - // If it doesn't exist, we create an unlimited hash as our initial key in the form of the hash key + the unlimited constant - // and assign this to the route to hash cache - // ex: this.RoutesToHashes[POST:/channels/channel_id/messages] = POST:/channels/channel_id/messages:unlimited - var hash = this.RoutesToHashes.GetOrAdd(hashKey, RateLimitBucket.GenerateUnlimitedHash(method, route)); - - // Next we use the hash to generate the key to obtain the bucket. - // ex: 80c17d2f203122d936070c88c8d10f33:guild_id:506128773926879242:webhook_id - // or if unlimited: POST:/channels/channel_id/messages:unlimited:guild_id:506128773926879242:webhook_id - var bucketId = RateLimitBucket.GenerateBucketId(hash, guild_id, channel_id, webhook_id); - - // If it's not in cache, create a new bucket and index it by its bucket id. - var bucket = this.HashesToBuckets.GetOrAdd(bucketId, new RateLimitBucket(hash, guild_id, channel_id, webhook_id)); - - bucket.LastAttemptAt = DateTimeOffset.UtcNow; - - // Cache the routes for each bucket so it can be used for GC later. - if (!bucket.RouteHashes.Contains(bucketId)) - bucket.RouteHashes.Add(bucketId); - - // Add the current route to the request queue, which indexes the amount - // of requests occurring to the bucket id. - _ = this.RequestQueue.TryGetValue(bucketId, out var count); - - // Increment by one atomically due to concurrency - this.RequestQueue[bucketId] = Interlocked.Increment(ref count); - - // Start bucket cleaner if not already running. - if (!this._cleanerRunning) - { - this._cleanerRunning = true; - this._bucketCleanerTokenSource = new CancellationTokenSource(); - this._cleanerTask = Task.Run(this.CleanupBucketsAsync, this._bucketCleanerTokenSource.Token); - this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task started."); - } - - url = RouteArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]); - return bucket; - } + internal RestClient(BaseDiscordClient client) + : this + ( + client.Configuration.Proxy, + client.Configuration.HttpTimeout, + client.Logger + ) + { + this.Discord = client; + this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client)); + this.HttpClient.BaseAddress = new(Endpoints.BASE_URI); + } - public Task ExecuteRequestAsync(BaseRestRequest request) - => request == null ? throw new ArgumentNullException(nameof(request)) : this.ExecuteRequestAsync(request, null, null); + // This is for meta-clients, such as the webhook client + internal RestClient + ( + IWebProxy proxy, + TimeSpan timeout, + ILogger logger + ) + { + this.Logger = logger; - // to allow proper rescheduling of the first request from a bucket - private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource ratelimitTcs) + HttpClientHandler httphandler = new() { - if (this._disposed) - return; - - HttpResponseMessage res = default; - - try - { - await this.GlobalRateLimitEvent.WaitAsync(); - - if (bucket == null) - bucket = request.RateLimitBucket; - - if (ratelimitTcs == null) - ratelimitTcs = await this.WaitForInitialRateLimit(bucket); - - if (ratelimitTcs == null) // ckeck rate limit only if we are not the probe request - { - var now = DateTimeOffset.UtcNow; - - await bucket.TryResetLimitAsync(now); - - // Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket - if (Interlocked.Decrement(ref bucket._remaining) < 0) - { - this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {Bucket} is blocked", bucket.ToString()); - var delay = bucket.Reset - now; - var resetDate = bucket.Reset; - - if (this.UseResetAfter) - { - delay = bucket.ResetAfter.Value; - resetDate = bucket.ResetAfterOffset; - } - - if (delay < new TimeSpan(-TimeSpan.TicksPerMinute)) - { - this.Logger.LogError(LoggerEvents.RatelimitDiag, "Failed to retrieve ratelimits - giving up and allowing next request for bucket"); - bucket._remaining = 1; - } - - if (delay < TimeSpan.Zero) - delay = TimeSpan.FromMilliseconds(100); - - this.Logger.LogWarning(LoggerEvents.RatelimitPreemptive, "Pre-emptive ratelimit triggered - waiting until {0:yyyy-MM-dd HH:mm:ss zzz} ({1:c}).", resetDate, delay); - Task.Delay(delay) - .ContinueWith(_ => this.ExecuteRequestAsync(request, null, null)) - .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); - - return; - } - this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {Bucket} is allowed", bucket.ToString()); - } - else - this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Initial request for {Bucket} is allowed", bucket.ToString()); - - var req = this.BuildRequest(request); - var response = new RestResponse(); - try - { - if (this._disposed) - return; - - res = await this.HttpClient.SendAsync(req, HttpCompletionOption.ResponseContentRead, CancellationToken.None); - - var bts = await res.Content.ReadAsByteArrayAsync(); - var txt = Utilities.UTF8.GetString(bts, 0, bts.Length); - - this.Logger.LogTrace(LoggerEvents.RestRx, txt); - - response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); - response.Response = txt; - response.ResponseCode = (int)res.StatusCode; - } - catch (HttpRequestException httpex) - { - this.Logger.LogError(LoggerEvents.RestError, httpex, "Request to {Url} triggered an HttpException", request.Url); - request.SetFaulted(httpex); - this.FailInitialRateLimitTest(request, ratelimitTcs); - return; - } - - this.UpdateBucket(request, response, ratelimitTcs); - - Exception ex = null; - switch (response.ResponseCode) - { - case 400: - case 405: - ex = new BadRequestException(request, response); - break; - - case 401: - case 403: - ex = new UnauthorizedException(request, response); - break; - - case 404: - ex = new NotFoundException(request, response); - break; - - case 413: - ex = new RequestSizeException(request, response); - break; - - case 429: - ex = new RateLimitException(request, response); - - // check the limit info and requeue - this.Handle429(response, out var wait, out var global); - if (wait != null) - { - if (global) - { - this.Logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down"); - try - { - this.GlobalRateLimitEvent.Reset(); - await wait; - } - finally - { - // we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here - _ = this.GlobalRateLimitEvent.SetAsync(); - } - this.ExecuteRequestAsync(request, bucket, ratelimitTcs) - .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); - } - else - { - this.Logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeueing request to {Url}", request.Url); - await wait; - this.ExecuteRequestAsync(request, bucket, ratelimitTcs) - .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); - } - - return; - } - break; - - case 500: - case 502: - case 503: - case 504: - ex = new ServerErrorException(request, response); - break; - } - - if (ex != null) - request.SetFaulted(ex); - else - request.SetCompleted(response); - } - catch (Exception ex) - { - this.Logger.LogError(LoggerEvents.RestError, ex, "Request to {Url} triggered an exception", request.Url); + UseCookies = false, + AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, + UseProxy = proxy != null, + Proxy = proxy + }; - // if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run - if (bucket != null && ratelimitTcs != null && bucket._limitTesting != 0) - this.FailInitialRateLimitTest(request, ratelimitTcs); - - if (!request.TrySetFaulted(ex)) - throw; - } - finally - { - res?.Dispose(); - - // Get and decrement active requests in this bucket by 1. - _ = this.RequestQueue.TryGetValue(bucket.BucketId, out var count); - this.RequestQueue[bucket.BucketId] = Interlocked.Decrement(ref count); - - // If it's 0 or less, we can remove the bucket from the active request queue, - // along with any of its past routes. - if (count <= 0) - { - foreach (var r in bucket.RouteHashes) - { - if (this.RequestQueue.ContainsKey(r)) - { - _ = this.RequestQueue.TryRemove(r, out _); - } - } - } - } - } - - private void FailInitialRateLimitTest(BaseRestRequest request, TaskCompletionSource ratelimitTcs, bool resetToInitial = false) + this.HttpClient = new HttpClient(httphandler) { - if (ratelimitTcs == null && !resetToInitial) - return; - - var bucket = request.RateLimitBucket; - - bucket._limitValid = false; - bucket._limitTestFinished = null; - bucket._limitTesting = 0; - - //Reset to initial values. - if (resetToInitial) - { - this.UpdateHashCaches(request, bucket); - bucket.Maximum = 0; - bucket._remaining = 0; - return; - } - - // no need to wait on all the potentially waiting tasks - _ = Task.Run(() => ratelimitTcs.TrySetResult(false)); - } + BaseAddress = new Uri(Utilities.GetApiBaseUri()), + Timeout = timeout + }; + + this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); + this.HttpClient.BaseAddress = new(Endpoints.BASE_URI); + + this.GlobalRateLimitEvent = new AsyncManualResetEvent(true); + + // retrying forever is rather suboptimal, but it's the old behaviour. We should discuss whether + // we want to break this. + AsyncRetryPolicy retry = Policy + .HandleResult + ( + result => result.Headers.Any(xm => xm.Key == "DSharpPlus-Internal-Response") + || result.StatusCode == HttpStatusCode.TooManyRequests + ) + .RetryForeverAsync(); + + this.RateLimitPolicy = Policy.WrapAsync(retry, new RateLimitPolicy(logger)); + } - private async Task> WaitForInitialRateLimit(RateLimitBucket bucket) + internal async ValueTask ExecuteRequestAsync + ( + TRequest request + ) + where TRequest : struct, IRestRequest + { + if (this._disposed) { - while (!bucket._limitValid) - { - if (bucket._limitTesting == 0) - { - if (Interlocked.CompareExchange(ref bucket._limitTesting, 1, 0) == 0) - { - // if we got here when the first request was just finishing, we must not create the waiter task as it would signal ExecuteRequestAsync to bypass rate limiting - if (bucket._limitValid) - return null; - - // allow exactly one request to go through without having rate limits available - var ratelimitsTcs = new TaskCompletionSource(); - bucket._limitTestFinished = ratelimitsTcs.Task; - return ratelimitsTcs; - } - } - // it can take a couple of cycles for the task to be allocated, so wait until it happens or we are no longer probing for the limits - Task waitTask = null; - while (bucket._limitTesting != 0 && (waitTask = bucket._limitTestFinished) == null) - await Task.Yield(); - if (waitTask != null) - await waitTask; - - // if the request failed and the response did not have rate limit headers we have allow the next request and wait again, thus this is a loop here - } - return null; + throw new ObjectDisposedException + ( + "DSharpPlus Rest Client", + "The Rest Client was disposed. No further requests are possible." + ); } - private HttpRequestMessage BuildRequest(BaseRestRequest request) + try { - var req = new HttpRequestMessage(new HttpMethod(request.Method.ToString()), request.Url); - if (request.Headers != null && request.Headers.Any()) - foreach (var kvp in request.Headers) - req.Headers.Add(kvp.Key, kvp.Value); + await this.GlobalRateLimitEvent.WaitAsync(); - if (request is RestRequest nmprequest && !string.IsNullOrWhiteSpace(nmprequest.Payload)) - { - this.Logger.LogTrace(LoggerEvents.RestTx, nmprequest.Payload); - - req.Content = new StringContent(nmprequest.Payload); - req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - } + using HttpRequestMessage req = request.Build(); - if (request is MultipartWebRequest mprequest) + Context context = new() { - this.Logger.LogTrace(LoggerEvents.RestTx, ""); - if (mprequest.Values.TryGetValue("payload_json", out var payload)) - this.Logger.LogTrace(LoggerEvents.RestTx, payload); - - var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); - - req.Headers.Add("Connection", "keep-alive"); - req.Headers.Add("Keep-Alive", "600"); - - var content = new MultipartFormDataContent(boundary); - if (mprequest.Values != null && mprequest.Values.Any()) - foreach (var kvp in mprequest.Values) - content.Add(new StringContent(kvp.Value), kvp.Key); + ["route"] = request.Route, + ["exempt-from-global-limit"] = request.IsExemptFromGlobalLimit + }; - if (mprequest.Files != null && mprequest.Files.Any()) - { - var i = 1; - foreach (var f in mprequest.Files) - { - var sc = new StreamContent(f.Stream); + using HttpResponseMessage response = await this.RateLimitPolicy.ExecuteAsync + ( + async (_) => await this.HttpClient.SendAsync + ( + req, + HttpCompletionOption.ResponseContentRead, + CancellationToken.None + ), + context + ); - if (f.ContentType != null) - sc.Headers.ContentType = new MediaTypeHeaderValue(f.ContentType); + string content = await response.Content.ReadAsStringAsync(); - if (f.FileType != null) - f.FileName += '.' + f.FileType; + this.Logger.LogTrace(LoggerEvents.RestRx, "{content}", content); - var count = !mprequest._removeFileCount ? i++.ToString(CultureInfo.InvariantCulture) : string.Empty; + _ = response.StatusCode switch + { + HttpStatusCode.BadRequest or HttpStatusCode.MethodNotAllowed => + throw new BadRequestException(req, response, content), - content.Add(sc, $"file{count}", f.FileName); - } - } + HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden => + throw new UnauthorizedException(req, response, content), - req.Content = content; - } + HttpStatusCode.NotFound => + throw new NotFoundException(req, response, content), - return req; - } + HttpStatusCode.RequestEntityTooLarge => + throw new RequestSizeException(req, response, content), - private void Handle429(RestResponse response, out Task wait_task, out bool global) - { - wait_task = null; - global = false; + HttpStatusCode.TooManyRequests => + throw new RateLimitException(req, response, content), - if (response.Headers == null) - return; - var hs = response.Headers; + HttpStatusCode.InternalServerError + or HttpStatusCode.BadGateway + or HttpStatusCode.ServiceUnavailable + or HttpStatusCode.GatewayTimeout => + throw new ServerErrorException(req, response, content), - // handle the wait - if (hs.TryGetValue("Retry-After", out var retry_after_raw)) - { - var retry_after = TimeSpan.FromSeconds(int.Parse(retry_after_raw, CultureInfo.InvariantCulture)); - wait_task = Task.Delay(retry_after); - } + // we need to keep the c# compiler happy, and not all branches can/should throw here. + _ => 0 + }; - // check if global b1nzy - if (hs.TryGetValue("X-RateLimit-Global", out var isglobal) && isglobal.Equals("true", StringComparison.InvariantCultureIgnoreCase)) + return new RestResponse() { - // global - global = true; - } + Response = content, + ResponseCode = response.StatusCode + }; } - - private void UpdateBucket(BaseRestRequest request, RestResponse response, TaskCompletionSource ratelimitTcs) + catch (Exception ex) { - var bucket = request.RateLimitBucket; - - if (response.Headers == null) - { - if (response.ResponseCode != 429) // do not fail when ratelimit was or the next request will be scheduled hitting the rate limit again - this.FailInitialRateLimitTest(request, ratelimitTcs); - return; - } - - var hs = response.Headers; - - if (hs.TryGetValue("X-RateLimit-Global", out var isglobal) && isglobal.Equals("true", StringComparison.InvariantCultureIgnoreCase)) - { - if (response.ResponseCode != 429) - this.FailInitialRateLimitTest(request, ratelimitTcs); - - return; - } - - var r1 = hs.TryGetValue("X-RateLimit-Limit", out var usesmax); - var r2 = hs.TryGetValue("X-RateLimit-Remaining", out var usesleft); - var r3 = hs.TryGetValue("X-RateLimit-Reset", out var reset); - var r4 = hs.TryGetValue("X-Ratelimit-Reset-After", out var resetAfter); - var r5 = hs.TryGetValue("X-Ratelimit-Bucket", out var hash); - - if (!r1 || !r2 || !r3 || !r4) - { - //If the limits were determined before this request, make the bucket initial again. - if (response.ResponseCode != 429) - this.FailInitialRateLimitTest(request, ratelimitTcs, ratelimitTcs == null); - - return; - } - - var clienttime = DateTimeOffset.UtcNow; - var resettime = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(double.Parse(reset, CultureInfo.InvariantCulture)); - var servertime = clienttime; - if (hs.TryGetValue("Date", out var raw_date)) - servertime = DateTimeOffset.Parse(raw_date, CultureInfo.InvariantCulture).ToUniversalTime(); - - var resetdelta = resettime - servertime; - //var difference = clienttime - servertime; - //if (Math.Abs(difference.TotalSeconds) >= 1) - //// this.Logger.LogMessage(LogLevel.DebugBaseDiscordClient.RestEventId, $"Difference between machine and server time: {difference.TotalMilliseconds.ToString("#,##0.00", CultureInfo.InvariantCulture)}ms", DateTime.Now); - //else - // difference = TimeSpan.Zero; - - if (request.RateLimitWaitOverride.HasValue) - resetdelta = TimeSpan.FromSeconds(request.RateLimitWaitOverride.Value); - var newReset = clienttime + resetdelta; - - if (this.UseResetAfter) - { - bucket.ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter, CultureInfo.InvariantCulture)); - newReset = clienttime + bucket.ResetAfter.Value + (request.RateLimitWaitOverride.HasValue - ? resetdelta - : TimeSpan.Zero); - bucket.ResetAfterOffset = newReset; - } - else - bucket.Reset = newReset; - - var maximum = int.Parse(usesmax, CultureInfo.InvariantCulture); - var remaining = int.Parse(usesleft, CultureInfo.InvariantCulture); - - if (ratelimitTcs != null) - { - // initial population of the ratelimit data - bucket.SetInitialValues(maximum, remaining, newReset); - - _ = Task.Run(() => ratelimitTcs.TrySetResult(true)); - } - else - { - // only update the bucket values if this request was for a newer interval than the one - // currently in the bucket, to avoid issues with concurrent requests in one bucket - // remaining is reset by TryResetLimit and not the response, just allow that to happen when it is time - if (bucket._nextReset == 0) - bucket._nextReset = newReset.UtcTicks; - } - - this.UpdateHashCaches(request, bucket, hash); + this.Logger.LogError + ( + LoggerEvents.RestError, + ex, + "Request to {url} triggered an exception", + $"{Endpoints.BASE_URI}/{request.Url}" + ); + + throw; } + } - private void UpdateHashCaches(BaseRestRequest request, RateLimitBucket bucket, string newHash = null) + public void Dispose() + { + if (this._disposed) { - var hashKey = RateLimitBucket.GenerateHashKey(request.Method, request.Route); - - if (!this.RoutesToHashes.TryGetValue(hashKey, out var oldHash)) - return; - - // This is an unlimited bucket, which we don't need to keep track of. - if (newHash == null) - { - _ = this.RoutesToHashes.TryRemove(hashKey, out _); - _ = this.HashesToBuckets.TryRemove(bucket.BucketId, out _); - return; - } - - // Only update the hash once, due to a bug on Discord's end. - // This will cause issues if the bucket hashes are dynamically changed from the API while running, - // in which case, Dispose will need to be called to clear the caches. - if (bucket._isUnlimited && newHash != oldHash) - { - this.Logger.LogDebug(LoggerEvents.RestHashMover, "Updating hash in {Hash}: \"{OldHash}\" -> \"{NewHash}\"", hashKey, oldHash, newHash); - var bucketId = RateLimitBucket.GenerateBucketId(newHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); - - _ = this.RoutesToHashes.AddOrUpdate(hashKey, newHash, (key, oldHash) => - { - bucket.Hash = newHash; - - var oldBucketId = RateLimitBucket.GenerateBucketId(oldHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); - - // Remove the old unlimited bucket. - _ = this.HashesToBuckets.TryRemove(oldBucketId, out _); - _ = this.HashesToBuckets.AddOrUpdate(bucketId, bucket, (key, oldBucket) => bucket); - - return newHash; - }); - } - return; } - private async Task CleanupBucketsAsync() - { - while (!this._bucketCleanerTokenSource.IsCancellationRequested) - { - try - { - await Task.Delay(this._bucketCleanupDelay, this._bucketCleanerTokenSource.Token); - } - catch { } - - if (this._disposed) - return; - - //Check and clean request queue first in case it wasn't removed properly during requests. - foreach (var key in this.RequestQueue.Keys) - { - var bucket = this.HashesToBuckets.Values.FirstOrDefault(x => x.RouteHashes.Contains(key)); - - if (bucket == null || (bucket != null && bucket.LastAttemptAt.AddSeconds(5) < DateTimeOffset.UtcNow)) - _ = this.RequestQueue.TryRemove(key, out _); - } - - var removedBuckets = 0; - StringBuilder bucketIdStrBuilder = default; - - foreach (var kvp in this.HashesToBuckets) - { - if (bucketIdStrBuilder == null) - bucketIdStrBuilder = new StringBuilder(); - - var key = kvp.Key; - var value = kvp.Value; - - // Don't remove the bucket if it's currently being handled by the rest client, unless it's an unlimited bucket. - if (this.RequestQueue.ContainsKey(value.BucketId) && !value._isUnlimited) - continue; - - var resetOffset = this.UseResetAfter ? value.ResetAfterOffset : value.Reset; - - // Don't remove the bucket if it's reset date is less than now + the additional wait time, unless it's an unlimited bucket. - if (resetOffset != null && !value._isUnlimited && (resetOffset > DateTimeOffset.UtcNow || DateTimeOffset.UtcNow - resetOffset < this._bucketCleanupDelay)) - continue; + this._disposed = true; - _ = this.HashesToBuckets.TryRemove(key, out _); - removedBuckets++; - bucketIdStrBuilder.Append(value.BucketId + ", "); - } + this.GlobalRateLimitEvent.Reset(); - if (removedBuckets > 0) - this.Logger.LogDebug(LoggerEvents.RestCleaner, "Removed {BucketCount} unused bucket{BucketPlural}: [{BucketId}]", removedBuckets, removedBuckets > 1 ? "s" : string.Empty, bucketIdStrBuilder.ToString().TrimEnd(',', ' ')); - - if (this.HashesToBuckets.Count == 0) - break; - } - - if (!this._bucketCleanerTokenSource.IsCancellationRequested) - this._bucketCleanerTokenSource.Cancel(); - - this._cleanerRunning = false; - this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); - } - - public void Dispose() + try { - if (this._disposed) - return; - - this._disposed = true; - - this.GlobalRateLimitEvent?.Reset(); - - if (this._bucketCleanerTokenSource?.IsCancellationRequested == false) - { - this._bucketCleanerTokenSource?.Cancel(); - this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); - } - - try - { - this._cleanerTask?.Dispose(); - this._bucketCleanerTokenSource?.Dispose(); - this.HttpClient?.Dispose(); - } - catch { } - - this.RoutesToHashes?.Clear(); - this.HashesToBuckets?.Clear(); - this.RequestQueue?.Clear(); + this.HttpClient?.Dispose(); } + catch { } } } - // More useless comments, sorry.. // Was listening to this, felt like sharing. // https://www.youtube.com/watch?v=ePX5qgDe9s4 diff --git a/DSharpPlus/Net/Rest/RestRequest.cs b/DSharpPlus/Net/Rest/RestRequest.cs index 514de8a3a8..efe702f55e 100644 --- a/DSharpPlus/Net/Rest/RestRequest.cs +++ b/DSharpPlus/Net/Rest/RestRequest.cs @@ -1,22 +1,61 @@ -using System; using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; -namespace DSharpPlus.Net +namespace DSharpPlus.Net; + +/// +/// Represents a non-multipart HTTP request. +/// +internal readonly record struct RestRequest : IRestRequest { + /// + public string Url { get; init; } + + /// + /// The method for this request. + /// + public HttpMethod Method { get; init; } + + /// + public string Route { get; init; } + + /// + public bool IsExemptFromGlobalLimit { get; init; } + + /// + /// The headers for this request. + /// + public IReadOnlyDictionary? Headers { get; init; } + /// - /// Represents a non-multipart HTTP request. + /// The payload sent with this request. /// - internal sealed class RestRequest : BaseRestRequest + public string? Payload { get; init; } + + /// + public HttpRequestMessage Build() { - /// - /// Gets the payload sent with this request. - /// - public string Payload { get; } + HttpRequestMessage request = new() + { + Method = this.Method, + RequestUri = new($"{Endpoints.BASE_URI}/{this.Url}") + }; - internal RestRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null) - : base(client, bucket, url, method, route, headers, ratelimitWaitOverride) + if (this.Payload is not null) { - this.Payload = payload; + request.Content = new StringContent(this.Payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); } + + if(this.Headers is not null) + { + foreach(KeyValuePair header in this.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + } + + return request; } } diff --git a/DSharpPlus/Net/Rest/RestRequestMethod.cs b/DSharpPlus/Net/Rest/RestRequestMethod.cs deleted file mode 100644 index 0356be36d0..0000000000 --- a/DSharpPlus/Net/Rest/RestRequestMethod.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace DSharpPlus.Net -{ - /// - /// Defines the HTTP method to use for an HTTP request. - /// - public enum RestRequestMethod : int - { - /// - /// Defines that the request is a GET request. - /// - GET = 0, - - /// - /// Defines that the request is a POST request. - /// - POST = 1, - - /// - /// Defines that the request is a DELETE request. - /// - DELETE = 2, - - /// - /// Defines that the request is a PATCH request. - /// - PATCH = 3, - - /// - /// Defines that the request is a PUT request. - /// - PUT = 4, - - /// - /// Defines that the request is a HEAD request. - /// - HEAD = 5 - } -} diff --git a/DSharpPlus/Net/Rest/RestResponse.cs b/DSharpPlus/Net/Rest/RestResponse.cs index c33bcafe7d..443ed3665e 100644 --- a/DSharpPlus/Net/Rest/RestResponse.cs +++ b/DSharpPlus/Net/Rest/RestResponse.cs @@ -1,27 +1,19 @@ -using System.Collections.Generic; +using System.Net; -namespace DSharpPlus.Net +namespace DSharpPlus.Net; + +/// +/// Represents a response sent by the remote HTTP party. +/// +public record struct RestResponse { /// - /// Represents a response sent by the remote HTTP party. + /// Gets the response code sent by the remote party. /// - public sealed class RestResponse - { - /// - /// Gets the response code sent by the remote party. - /// - public int ResponseCode { get; internal set; } - - /// - /// Gets the headers sent by the remote party. - /// - public IReadOnlyDictionary Headers { get; internal set; } - - /// - /// Gets the contents of the response sent by the remote party. - /// - public string Response { get; internal set; } + public HttpStatusCode? ResponseCode { get; internal set; } - internal RestResponse() { } - } + /// + /// Gets the contents of the response sent by the remote party. + /// + public string? Response { get; internal set; } } diff --git a/DSharpPlus/Net/Rest/SessionBucket.cs b/DSharpPlus/Net/Rest/SessionBucket.cs index b0fa4d4d65..e7d340b4f9 100644 --- a/DSharpPlus/Net/Rest/SessionBucket.cs +++ b/DSharpPlus/Net/Rest/SessionBucket.cs @@ -1,45 +1,41 @@ using System; using Newtonsoft.Json; -namespace DSharpPlus.Net +namespace DSharpPlus.Net; + +/// +/// Represents the bucket limits for identifying to Discord. +/// This is only relevant for clients that are manually sharding. +/// +public class SessionBucket { /// - /// Represents the bucket limits for identifying to Discord. - /// This is only relevant for clients that are manually sharding. + /// Gets the total amount of sessions per token. /// - public class SessionBucket - { - /// - /// Gets the total amount of sessions per token. - /// - [JsonProperty("total")] - public int Total { get; internal set; } - - /// - /// Gets the remaining amount of sessions for this token. - /// - [JsonProperty("remaining")] - public int Remaining { get; internal set; } + [JsonProperty("total")] + public int Total { get; internal set; } - /// - /// Gets the datetime when the will reset. - /// - [JsonIgnore] - public DateTimeOffset ResetAfter { get; internal set; } + /// + /// Gets the remaining amount of sessions for this token. + /// + [JsonProperty("remaining")] + public int Remaining { get; internal set; } - /// - /// Gets the maximum amount of shards that can boot concurrently. - /// - [JsonProperty("max_concurrency")] - public int MaxConcurrency { get; internal set; } + /// + /// Gets the datetime when the will reset. + /// + [JsonIgnore] + public DateTimeOffset ResetAfter { get; internal set; } - [JsonProperty("reset_after")] - internal int ResetAfterInternal { get; set; } + /// + /// Gets the maximum amount of shards that can boot concurrently. + /// + [JsonProperty("max_concurrency")] + public int MaxConcurrency { get; internal set; } -#pragma warning disable CS0114 - public override string ToString() - => $"[{this.Remaining}/{this.Total}] {this.ResetAfter}. {this.MaxConcurrency}x concurrency"; + [JsonProperty("reset_after")] + internal int ResetAfterInternal { get; set; } -#pragma warning restore CS0114 - } + public override string ToString() + => $"[{this.Remaining}/{this.Total}] {this.ResetAfter}. {this.MaxConcurrency}x concurrency"; } diff --git a/DSharpPlus/QueryUriBuilder.cs b/DSharpPlus/QueryUriBuilder.cs index f7f3b3088a..62cdca9213 100644 --- a/DSharpPlus/QueryUriBuilder.cs +++ b/DSharpPlus/QueryUriBuilder.cs @@ -2,45 +2,46 @@ using System.Collections.Generic; using System.Linq; -namespace DSharpPlus -{ - internal class QueryUriBuilder - { - public Uri SourceUri { get; } +namespace DSharpPlus; - public IReadOnlyList> QueryParameters => this._queryParams; - private readonly List> _queryParams = new(); - - public QueryUriBuilder(string uri) - { - if (uri == null) - throw new ArgumentNullException(nameof(uri)); +internal class QueryUriBuilder +{ + public string SourceUri { get; } - this.SourceUri = new Uri(uri); - } + public IReadOnlyList> QueryParameters => this._queryParams; + private readonly List> _queryParams = new(); - public QueryUriBuilder(Uri uri) - { - if (uri == null) - throw new ArgumentNullException(nameof(uri)); + public QueryUriBuilder(string uri) + { + ArgumentNullException.ThrowIfNull(uri, nameof(uri)); - this.SourceUri = uri; - } + this.SourceUri = uri; + } - public QueryUriBuilder AddParameter(string key, string value) + public QueryUriBuilder AddParameter(string key, string? value) + { + if (value is null) { - this._queryParams.Add(new KeyValuePair(key, value)); return this; } - public Uri Build() - { - return new UriBuilder(this.SourceUri) - { - Query = string.Join("&", this._queryParams.Select(e => Uri.EscapeDataString(e.Key) + '=' + Uri.EscapeDataString(e.Value))) - }.Uri; - } + this._queryParams.Add(new KeyValuePair(key, value)); + return this; + } - public override string ToString() => this.Build().ToString(); + public string Build() + { + string query = string.Join + ( + "&", + this._queryParams.Select + ( + e => Uri.EscapeDataString(e.Key) + '=' + Uri.EscapeDataString(e.Value) + ) + ); + + return $"{this.SourceUri}?{query}"; } + + public override string ToString() => this.Build().ToString(); } diff --git a/DSharpPlus/Utilities.cs b/DSharpPlus/Utilities.cs index d09572397c..a7c2012867 100644 --- a/DSharpPlus/Utilities.cs +++ b/DSharpPlus/Utilities.cs @@ -7,302 +7,324 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; + using DSharpPlus.Entities; using DSharpPlus.Net; + using Microsoft.Extensions.Logging; -namespace DSharpPlus +namespace DSharpPlus; + +/// +/// Various Discord-related utilities. +/// +public static class Utilities { /// - /// Various Discord-related utilities. + /// Gets the version of the library /// - public static class Utilities - { - /// - /// Gets the version of the library - /// - private static string VersionHeader { get; set; } - private static Dictionary PermissionStrings { get; set; } + private static string VersionHeader { get; set; } + private static Dictionary PermissionStrings { get; set; } - internal static UTF8Encoding UTF8 { get; } = new UTF8Encoding(false); + internal static UTF8Encoding UTF8 { get; } = new UTF8Encoding(false); - static Utilities() - { - PermissionStrings = new Dictionary(); - var t = typeof(Permissions); - var ti = t.GetTypeInfo(); - var vals = Enum.GetValues(t).Cast(); - - foreach (var xv in vals) - { - var xsv = xv.ToString(); - var xmv = ti.DeclaredMembers.FirstOrDefault(xm => xm.Name == xsv); - var xav = xmv.GetCustomAttribute(); + static Utilities() + { + PermissionStrings = new Dictionary(); + Type t = typeof(Permissions); + TypeInfo ti = t.GetTypeInfo(); + IEnumerable vals = Enum.GetValues(t).Cast(); - PermissionStrings[xv] = xav.String; - } + foreach (Permissions xv in vals) + { + string xsv = xv.ToString(); + MemberInfo? xmv = ti.DeclaredMembers.FirstOrDefault(xm => xm.Name == xsv); + PermissionStringAttribute? xav = xmv.GetCustomAttribute(); - var a = typeof(DiscordClient).GetTypeInfo().Assembly; + PermissionStrings[xv] = xav.String; + } - var vs = ""; - var iv = a.GetCustomAttribute(); - if (iv != null) - vs = iv.InformationalVersion; - else - { - var v = a.GetName().Version; - vs = v.ToString(3); - } + Assembly a = typeof(DiscordClient).GetTypeInfo().Assembly; - VersionHeader = $"DiscordBot (https://github.com/DSharpPlus/DSharpPlus, v{vs})"; + string vs = ""; + AssemblyInformationalVersionAttribute? iv = a.GetCustomAttribute(); + if (iv != null) + { + vs = iv.InformationalVersion; + } + else + { + Version? v = a.GetName().Version; + vs = v.ToString(3); } - internal static string GetApiBaseUri() - => Endpoints.BASE_URI; + VersionHeader = $"DiscordBot (https://github.com/DSharpPlus/DSharpPlus, v{vs})"; + } - internal static Uri GetApiUriFor(string path) - => new($"{GetApiBaseUri()}{path}"); + internal static string GetApiBaseUri() + => Endpoints.BASE_URI; - internal static Uri GetApiUriFor(string path, string queryString) - => new($"{GetApiBaseUri()}{path}{queryString}"); + internal static Uri GetApiUriFor(string path) + => new($"{GetApiBaseUri()}{path}"); - internal static QueryUriBuilder GetApiUriBuilderFor(string path) - => new($"{GetApiBaseUri()}{path}"); + internal static Uri GetApiUriFor(string path, string queryString) + => new($"{GetApiBaseUri()}{path}{queryString}"); - internal static string GetFormattedToken(BaseDiscordClient client) => GetFormattedToken(client.Configuration); + internal static QueryUriBuilder GetApiUriBuilderFor(string path) + => new($"{GetApiBaseUri()}{path}"); - internal static string GetFormattedToken(DiscordConfiguration config) + internal static string GetFormattedToken(BaseDiscordClient client) => GetFormattedToken(client.Configuration); + + internal static string GetFormattedToken(DiscordConfiguration config) + { + return config.TokenType switch { - return config.TokenType switch - { - TokenType.Bearer => $"Bearer {config.Token}", - TokenType.Bot => $"Bot {config.Token}", - _ => throw new ArgumentException("Invalid token type specified.", nameof(config.Token)), - }; - } + TokenType.Bearer => $"Bearer {config.Token}", + TokenType.Bot => $"Bot {config.Token}", + _ => throw new ArgumentException("Invalid token type specified.", nameof(config.Token)), + }; + } - internal static Dictionary GetBaseHeaders() - => new(); + internal static string GetUserAgent() + => VersionHeader; - internal static string GetUserAgent() - => VersionHeader; + internal static bool ContainsUserMentions(string message) + { + string pattern = @"<@(\d+)>"; + Regex regex = new Regex(pattern, RegexOptions.ECMAScript); + return regex.IsMatch(message); + } - internal static bool ContainsUserMentions(string message) - { - var pattern = @"<@(\d+)>"; - var regex = new Regex(pattern, RegexOptions.ECMAScript); - return regex.IsMatch(message); - } + internal static bool ContainsNicknameMentions(string message) + { + string pattern = @"<@!(\d+)>"; + Regex regex = new Regex(pattern, RegexOptions.ECMAScript); + return regex.IsMatch(message); + } - internal static bool ContainsNicknameMentions(string message) - { - var pattern = @"<@!(\d+)>"; - var regex = new Regex(pattern, RegexOptions.ECMAScript); - return regex.IsMatch(message); - } + internal static bool ContainsChannelMentions(string message) + { + string pattern = @"<#(\d+)>"; + Regex regex = new Regex(pattern, RegexOptions.ECMAScript); + return regex.IsMatch(message); + } - internal static bool ContainsChannelMentions(string message) - { - var pattern = @"<#(\d+)>"; - var regex = new Regex(pattern, RegexOptions.ECMAScript); - return regex.IsMatch(message); - } + internal static bool ContainsRoleMentions(string message) + { + string pattern = @"<@&(\d+)>"; + Regex regex = new Regex(pattern, RegexOptions.ECMAScript); + return regex.IsMatch(message); + } - internal static bool ContainsRoleMentions(string message) - { - var pattern = @"<@&(\d+)>"; - var regex = new Regex(pattern, RegexOptions.ECMAScript); - return regex.IsMatch(message); - } + internal static bool ContainsEmojis(string message) + { + string pattern = @""; + Regex regex = new Regex(pattern, RegexOptions.ECMAScript); + return regex.IsMatch(message); + } - internal static bool ContainsEmojis(string message) + internal static IEnumerable GetUserMentions(DiscordMessage message) + { + Regex regex = new Regex(@"<@!?(\d+)>", RegexOptions.ECMAScript); + MatchCollection matches = regex.Matches(message.Content); + foreach (Match match in matches) { - var pattern = @""; - var regex = new Regex(pattern, RegexOptions.ECMAScript); - return regex.IsMatch(message); + yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } + } - internal static IEnumerable GetUserMentions(DiscordMessage message) + internal static IEnumerable GetRoleMentions(DiscordMessage message) + { + Regex regex = new Regex(@"<@&(\d+)>", RegexOptions.ECMAScript); + MatchCollection matches = regex.Matches(message.Content); + foreach (Match match in matches) { - var regex = new Regex(@"<@!?(\d+)>", RegexOptions.ECMAScript); - var matches = regex.Matches(message.Content); - foreach (Match match in matches) - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } + } - internal static IEnumerable GetRoleMentions(DiscordMessage message) + internal static IEnumerable GetChannelMentions(DiscordMessage message) + { + Regex regex = new Regex(@"<#(\d+)>", RegexOptions.ECMAScript); + MatchCollection matches = regex.Matches(message.Content); + foreach (Match match in matches) { - var regex = new Regex(@"<@&(\d+)>", RegexOptions.ECMAScript); - var matches = regex.Matches(message.Content); - foreach (Match match in matches) - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } + } - internal static IEnumerable GetChannelMentions(DiscordMessage message) + internal static IEnumerable GetEmojis(DiscordMessage message) + { + Regex regex = new Regex(@"", RegexOptions.ECMAScript); + MatchCollection matches = regex.Matches(message.Content); + foreach (Match match in matches) { - var regex = new Regex(@"<#(\d+)>", RegexOptions.ECMAScript); - var matches = regex.Matches(message.Content); - foreach (Match match in matches) - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + yield return ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); } + } - internal static IEnumerable GetEmojis(DiscordMessage message) - { - var regex = new Regex(@"", RegexOptions.ECMAScript); - var matches = regex.Matches(message.Content); - foreach (Match match in matches) - yield return ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); - } + internal static bool IsValidSlashCommandName(string name) + { + Regex regex = new Regex(@"^[\w-]{1,32}$"); + return regex.IsMatch(name); + } - internal static bool IsValidSlashCommandName(string name) - { - var regex = new Regex(@"^[\w-]{1,32}$"); - return regex.IsMatch(name); - } + internal static bool HasMessageIntents(DiscordIntents intents) + => (intents.HasIntent(DiscordIntents.GuildMessages) && intents.HasIntent(DiscordIntents.MessageContents)) || intents.HasIntent(DiscordIntents.DirectMessages); - internal static bool HasMessageIntents(DiscordIntents intents) - => (intents.HasIntent(DiscordIntents.GuildMessages) && intents.HasIntent(DiscordIntents.MessageContents)) || intents.HasIntent(DiscordIntents.DirectMessages); + internal static bool HasReactionIntents(DiscordIntents intents) + => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); - internal static bool HasReactionIntents(DiscordIntents intents) - => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); + internal static bool HasTypingIntents(DiscordIntents intents) + => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); - internal static bool HasTypingIntents(DiscordIntents intents) - => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); + internal static bool IsTextableChannel(DiscordChannel channel) + => channel.Type switch + { + ChannelType.Text => true, + ChannelType.Voice => true, + ChannelType.Group => true, + ChannelType.Private => true, + ChannelType.PublicThread => true, + ChannelType.PrivateThread => true, + ChannelType.NewsThread => true, + ChannelType.News => true, + ChannelType.Stage => true, + _ => false, + }; + + + // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula + /// + /// Gets a shard id from a guild id and total shard count. + /// + /// The guild id the shard is on. + /// The total amount of shards. + /// The shard id. + public static int GetShardId(ulong guildId, int shardCount) + => (int)((guildId >> 22) % (ulong)shardCount); - internal static bool IsTextableChannel(DiscordChannel channel) - => channel.Type switch - { - ChannelType.Text => true, - ChannelType.Voice => true, - ChannelType.Group => true, - ChannelType.Private => true, - ChannelType.PublicThread => true, - ChannelType.PrivateThread => true, - ChannelType.NewsThread => true, - ChannelType.News => true, - ChannelType.Stage => true, - _ => false, - }; - - - // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula - /// - /// Gets a shard id from a guild id and total shard count. - /// - /// The guild id the shard is on. - /// The total amount of shards. - /// The shard id. - public static int GetShardId(ulong guildId, int shardCount) - => (int)((guildId >> 22) % (ulong)shardCount); - - /// - /// Helper method to create a from Unix time seconds for targets that do not support this natively. - /// - /// Unix time seconds to convert. - /// Whether the method should throw on failure. Defaults to true. - /// Calculated . - public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) + /// + /// Helper method to create a from Unix time seconds for targets that do not support this natively. + /// + /// Unix time seconds to convert. + /// Whether the method should throw on failure. Defaults to true. + /// Calculated . + public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) + { + try { - try + return DateTimeOffset.FromUnixTimeSeconds(unixTime); + } + catch (Exception) + { + if (shouldThrow) { - return DateTimeOffset.FromUnixTimeSeconds(unixTime); + throw; } - catch (Exception) - { - if (shouldThrow) - throw; - return DateTimeOffset.MinValue; - } + return DateTimeOffset.MinValue; } + } - /// - /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. - /// - /// Unix time milliseconds to convert. - /// Whether the method should throw on failure. Defaults to true. - /// Calculated . - public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) + /// + /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. + /// + /// Unix time milliseconds to convert. + /// Whether the method should throw on failure. Defaults to true. + /// Calculated . + public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) + { + try + { + return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); + } + catch (Exception) { - try + if (shouldThrow) { - return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); + throw; } - catch (Exception) - { - if (shouldThrow) - throw; - return DateTimeOffset.MinValue; - } + return DateTimeOffset.MinValue; } + } - /// - /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. - /// - /// to calculate Unix time for. - /// Calculated Unix time. - public static long GetUnixTime(DateTimeOffset dto) - => dto.ToUnixTimeMilliseconds(); - - /// - /// Computes a timestamp from a given snowflake. - /// - /// Snowflake to compute a timestamp from. - /// Computed timestamp. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) - => DiscordClient._discordEpoch.AddMilliseconds(snowflake >> 22); - - /// - /// Converts this into human-readable format. - /// - /// Permissions enumeration to convert. - /// Human-readable permissions. - public static string ToPermissionString(this Permissions perm) - { - if (perm == Permissions.None) - return PermissionStrings[perm]; - - perm &= PermissionMethods.FULL_PERMS; + /// + /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. + /// + /// to calculate Unix time for. + /// Calculated Unix time. + public static long GetUnixTime(DateTimeOffset dto) + => dto.ToUnixTimeMilliseconds(); - var strs = PermissionStrings - .Where(xkvp => xkvp.Key != Permissions.None && (perm & xkvp.Key) == xkvp.Key) - .Select(xkvp => xkvp.Value); + /// + /// Computes a timestamp from a given snowflake. + /// + /// Snowflake to compute a timestamp from. + /// Computed timestamp. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) + => DiscordClient._discordEpoch.AddMilliseconds(snowflake >> 22); - return string.Join(", ", strs.OrderBy(xs => xs)); + /// + /// Converts this into human-readable format. + /// + /// Permissions enumeration to convert. + /// Human-readable permissions. + public static string ToPermissionString(this Permissions perm) + { + if (perm == Permissions.None) + { + return PermissionStrings[perm]; } - /// - /// Checks whether this string contains given characters. - /// - /// String to check. - /// Characters to check for. - /// Whether the string contained these characters. - public static bool Contains(this string str, params char[] characters) - { - foreach (var xc in str) - if (characters.Contains(xc)) - return true; + perm &= PermissionMethods.FULL_PERMS; - return false; - } + IEnumerable strs = PermissionStrings + .Where(xkvp => xkvp.Key != Permissions.None && (perm & xkvp.Key) == xkvp.Key) + .Select(xkvp => xkvp.Value); - internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) + return string.Join(", ", strs.OrderBy(xs => xs)); + } + + /// + /// Checks whether this string contains given characters. + /// + /// String to check. + /// Characters to check for. + /// Whether the string contained these characters. + public static bool Contains(this string str, params char[] characters) + { + foreach (char xc in str) { - if (task == null) - throw new ArgumentNullException(nameof(task)); + if (characters.Contains(xc)) + { + return true; + } + } - if (logger == null) - return; + return false; + } - task.ContinueWith(t => logger.Log(level, eventId, t.Exception, message), TaskContinuationOptions.OnlyOnFaulted); + internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) + { + if (task == null) + { + throw new ArgumentNullException(nameof(task)); } - internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + if (logger == null) { - key = kvp.Key; - value = kvp.Value; + return; } + + task.ContinueWith(t => logger.Log(level, eventId, t.Exception, message), TaskContinuationOptions.OnlyOnFaulted); + } + + internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + { + key = kvp.Key; + value = kvp.Value; } } diff --git a/Directory.Packages.props b/Directory.Packages.props index c14484e667..201bfc9c72 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,6 @@ - @@ -11,9 +10,11 @@ - - + + + + - + \ No newline at end of file diff --git a/tools/AutoUpdateChannelDescription/Program.cs b/tools/AutoUpdateChannelDescription/Program.cs index 3e7fa4525c..4fbd1768ff 100644 --- a/tools/AutoUpdateChannelDescription/Program.cs +++ b/tools/AutoUpdateChannelDescription/Program.cs @@ -66,7 +66,7 @@ await channel.ModifyAsync(channel => } catch (DiscordException error) { - Console.WriteLine($"Error: HTTP {error.WebResponse.ResponseCode}, {error.WebResponse.Response}"); + Console.WriteLine($"Error: HTTP {error.Response!.StatusCode}, {await error.Response.Content.ReadAsStringAsync()}"); } await client.DisconnectAsync();