diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 973012622..3bfdcf870 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,2622 +1,2622 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Attributes; using DisCatSharp.ApplicationCommands.Context; using DisCatSharp.ApplicationCommands.Entities; using DisCatSharp.ApplicationCommands.Enums; using DisCatSharp.ApplicationCommands.EventArgs; using DisCatSharp.ApplicationCommands.Exceptions; using DisCatSharp.ApplicationCommands.Workers; using DisCatSharp.Common; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.Enums.Core; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; // ReSharper disable HeuristicUnreachableCode namespace DisCatSharp.ApplicationCommands; /// /// A class that handles slash commands for a client. /// public sealed class ApplicationCommandsExtension : BaseExtension { /// /// A list of methods for top level commands. /// internal static List CommandMethods { get; set; } = []; /// /// List of groups. /// internal static List GroupCommands { get; set; } = []; /// /// List of groups with subgroups. /// internal static List SubGroupCommands { get; set; } = []; /// /// List of context menus. /// internal static List ContextMenuCommands { get; set; } = []; /// /// List of global commands on discords backend. /// internal static List GlobalDiscordCommands { get; set; } = []; /// /// List of guild commands on discords backend. /// internal static Dictionary> GuildDiscordCommands { get; set; } = []; /// /// Singleton modules. /// private static List s_singletonModules { get; set; } = []; /// /// List of modules to register. /// private readonly List> _updateList = []; /// /// Configuration for Discord. /// internal static ApplicationCommandsConfiguration Configuration; /// /// Set to true if anything fails when registering. /// private static bool s_errored { get; set; } /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => s_registeredCommands.Select(guild => new KeyValuePair>(guild.Key, guild.Value .Select(parent => new RegisteredDiscordApplicationCommand(parent)).ToList())).ToList().AsReadOnly(); /// /// Sets a list of registered commands. The key is the guild id (null if global). /// private static readonly List>> s_registeredCommands = []; /// /// Gets a list of registered global commands. /// public IReadOnlyList GlobalCommands => GlobalCommandsInternal; /// /// Sets a list of registered global commands. /// internal static readonly List GlobalCommandsInternal = []; /// /// Gets a list of registered guild commands mapped by guild id. /// public IReadOnlyDictionary> GuildCommands => GuildCommandsInternal; /// /// Sets a list of registered guild commands mapped by guild id. /// internal static readonly Dictionary> GuildCommandsInternal = []; /// /// Gets the guild ids where the applications.commands scope is missing. /// private List MISSING_SCOPE_GUILD_IDS { get; set; } = []; /// /// Gets the guild ids where the applications.commands scope is missing. /// private static List s_missingScopeGuildIdsGlobal { get; set; } = []; /// /// Gets whether debug is enabled. /// internal static bool DebugEnabled { get; set; } /// /// Gets the debug level for the logs. /// internal static LogLevel ApplicationCommandsLogLevel => DebugEnabled ? LogLevel.Debug : LogLevel.Trace; /// /// Gets the logger. /// internal static ILogger Logger { get; set; } /// /// Gets whether check through all guilds is enabled. /// internal static bool CheckAllGuilds { get; set; } /// /// Gets whether the registration check should be manually overridden. /// internal static bool ManOr { get; set; } /// /// Gets whether interactions should be automatically deffered. /// internal static bool AutoDeferEnabled { get; set; } /// /// Whether this module finished the startup. /// internal bool ShardStartupFinished { get; set; } = false; /// /// Whether this module finished the startup. /// internal static bool StartupFinished { get; set; } = false; /// /// Gets the service provider this module was configured with. /// public IServiceProvider Services => Configuration.ServiceProvider; /// /// Whether this module is called by an unit test. /// internal static bool IsCalledByUnitTest { get; set; } = false; /// /// Gets a list of handled interactions. Fix for double interaction execution bug. /// internal static readonly List HandledInteractions = []; /// /// Gets the shard count. /// internal static int ShardCount { get; set; } = 1; /// /// Gets the count of shards who finished initializing the module. /// internal static int FinishedShardCount { get; set; } = 0; /// /// Gets whether the finish event was fired. /// public static bool FinishFired { get; set; } = false; /// /// Initializes a new instance of the class. /// /// The configuration. internal ApplicationCommandsExtension(ApplicationCommandsConfiguration? configuration = null) { configuration ??= new(); Configuration = configuration; DebugEnabled = configuration?.DebugStartup ?? false; CheckAllGuilds = configuration?.CheckAllGuilds ?? false; AutoDeferEnabled = configuration?.AutoDefer ?? false; IsCalledByUnitTest = configuration?.UnitTestMode ?? false; this._slashError = new("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null!); this._slashExecuted = new("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null!); this._contextMenuErrored = new("CONTEXTMENU_ERRORED", TimeSpan.Zero, null!); this._contextMenuExecuted = new("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null!); this._applicationCommandsModuleReady = new("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null!); this._applicationCommandsModuleStartupFinished = new("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null!); this._globalApplicationCommandsRegistered = new("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null!); this._guildApplicationCommandsRegistered = new("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null!); this.ApplicationCommandsModuleStartupFinished += this.CheckStartupFinishAsync; } /// /// Runs setup. /// DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. /// /// The client to setup on. protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; ShardCount = client.ShardCount; Logger = client.Logger; this.Client.Ready += (c, e) => { if (!this.ShardStartupFinished) _ = Task.Run(async () => await this.UpdateAsync().ConfigureAwait(false)); return Task.CompletedTask; }; this.Client.InteractionCreated += this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup; } /// /// Catches all interactions during the startup. /// /// The client. /// The interaction create event args. /// private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e) { if (!this.ShardStartupFinished) await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent("Attention: This application is still starting up. Application commands are unavailable for now.")).ConfigureAwait(false); else await Task.Delay(1).ConfigureAwait(false); } /// /// Catches all context menu interactions during the startup. /// /// The client. /// The context menu interaction create event args. private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e) { if (!this.ShardStartupFinished) await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now.")).ConfigureAwait(false); else await Task.Delay(1).ConfigureAwait(false); } /// /// Fired when the startup is completed. /// Switches the interaction handling from and to and . /// private void FinishedRegistration() { this.Client.InteractionCreated -= this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup; this.Client.InteractionCreated += this.InteractionHandler; this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; } /// /// Cleans the module for a new start of the bot. /// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES. /// public void CleanModule() { this._updateList.Clear(); s_singletonModules.Clear(); s_errored = false; ShardCount = 1; CommandMethods.Clear(); GroupCommands.Clear(); ContextMenuCommands.Clear(); SubGroupCommands.Clear(); s_singletonModules.Clear(); s_registeredCommands.Clear(); GlobalCommandsInternal.Clear(); GuildCommandsInternal.Clear(); s_missingScopeGuildIdsGlobal.Clear(); this.MISSING_SCOPE_GUILD_IDS.Clear(); HandledInteractions.Clear(); FinishedShardCount = 0; FinishFired = false; StartupFinished = false; this.ShardStartupFinished = false; this.Client.InteractionCreated += this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup; } /// /// Cleans all guild application commands. /// You normally don't need to execute it. /// public async Task CleanGuildCommandsAsync() { foreach (var guild in this.Client.Guilds.Values) await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()).ConfigureAwait(false); } /// /// Cleans all global application commands. /// You normally don't need to execute it. /// public async Task CleanGlobalCommandsAsync() => await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()).ConfigureAwait(false); /// /// Registers all commands from a given assembly. The command classes need to be public to be considered for registration. /// /// Assembly to register commands from. /// The guild id to register it on. public void RegisterGuildCommands(Assembly assembly, ulong guildId) { var types = assembly.GetTypes().Where(xt => { var xti = xt.GetTypeInfo(); return xti.IsModuleCandidateType() && !xti.IsNested; }); foreach (var xt in types) this.RegisterGuildCommands(xt, guildId, null); } /// /// Registers all commands from a given assembly. The command classes need to be public to be considered for registration. /// /// Assembly to register commands from. public void RegisterGlobalCommands(Assembly assembly) { var types = assembly.GetTypes().Where(xt => { var xti = xt.GetTypeInfo(); return xti.IsModuleCandidateType() && !xti.IsNested; }); foreach (var xt in types) this.RegisterGlobalCommands(xt, null); } /// /// Registers a command class with optional translation setup for a guild. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(ulong guildId, Action? translationSetup = null) where T : ApplicationCommandsModule => this._updateList.Add(new(guildId, new(typeof(T), translationSetup))); /// /// Registers a command class with optional translation setup for a guild. /// /// The of the command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(Type type, ulong guildId, Action? translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); this._updateList.Add(new(guildId, new(type, translationSetup))); } /// /// Registers a command class with optional translation setup globally. /// /// The command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Action? translationSetup = null) where T : ApplicationCommandsModule => this._updateList.Add(new(null, new(typeof(T), translationSetup))); /// /// Registers a command class with optional translation setup globally. /// /// The of the command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Type type, Action? translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); this._updateList.Add(new(null, new(type, translationSetup))); } /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { add => this._applicationCommandsModuleReady.Register(value); remove => this._applicationCommandsModuleReady.Unregister(value); } /// /// Fires the application command module ready event. /// This is fired when the whole module for the finished the startup and registration. /// private readonly AsyncEvent _applicationCommandsModuleReady; /// /// Fired when the application commands modules startup is finished. /// public event AsyncEventHandler ApplicationCommandsModuleStartupFinished { add => this._applicationCommandsModuleStartupFinished.Register(value); remove => this._applicationCommandsModuleStartupFinished.Unregister(value); } /// /// Fires the application command module startup finished event. /// private readonly AsyncEvent _applicationCommandsModuleStartupFinished; /// /// Fired when guild commands are registered on a guild. /// public event AsyncEventHandler GuildApplicationCommandsRegistered { add => this._guildApplicationCommandsRegistered.Register(value); remove => this._guildApplicationCommandsRegistered.Unregister(value); } /// /// Fires the guild application command registered event. /// private readonly AsyncEvent _guildApplicationCommandsRegistered; /// /// Fired when the global commands are registered. /// public event AsyncEventHandler GlobalApplicationCommandsRegistered { add => this._globalApplicationCommandsRegistered.Register(value); remove => this._globalApplicationCommandsRegistered.Unregister(value); } /// /// Fires the global application command registered event. /// private readonly AsyncEvent _globalApplicationCommandsRegistered; /// /// Used for RegisterCommands and the event. /// internal async Task UpdateAsync() { this.Client.Logger.Log(LogLevel.Information, "Request to register commands on shard {shard}", this.Client.ShardId); try { if (this.ShardStartupFinished) { this.Client.Logger.Log(LogLevel.Information, "Shard {shard} already setup, skipping", this.Client.ShardId); return; } GlobalDiscordCommands = []; GuildDiscordCommands = []; this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} has {guilds} guilds", this.Client.ShardId, this.Client.ReadyGuildIds.Count); List failedGuilds = []; var globalCommands = IsCalledByUnitTest ? null : (await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false).ConfigureAwait(false))?.ToList() ?? null; var guilds = CheckAllGuilds ? this.Client.ReadyGuildIds : this._updateList.Where(x => x.Key is not null)?.Select(x => x.Key.Value).Distinct().ToList(); var wrongShards = guilds is not null && this.Client.ReadyGuildIds.Count is not 0 ? guilds.Where(x => !this.Client.ReadyGuildIds.Contains(x)).ToList() : []; if (wrongShards.Count is not 0) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Some guilds are not on the same shard as the client. Removing them from the update list"); foreach (var guild in wrongShards) { this._updateList.RemoveAll(x => x.Key == guild); guilds?.Remove(guild); } } var commandsPending = this._updateList.Select(x => x.Key).Distinct().ToList(); if (guilds is not null && guilds.Count != 0) foreach (var guild in guilds) { List? commands = null; var unauthorized = false; try { commands = (await this.Client.GetGuildApplicationCommandsAsync(guild, Configuration?.EnableLocalization ?? false).ConfigureAwait(false)).ToList() ?? null; } catch (UnauthorizedException) { unauthorized = true; } finally { switch (unauthorized) { case false when commands is not null && commands.Count is not 0: GuildDiscordCommands.Add(guild, [.. commands]); break; case true: failedGuilds.Add(guild); break; } } } //Default should be to add the help and slash commands can be added without setting any configuration //so this should still add the default help if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp)) { this._updateList.Add(new(null, new(typeof(DefaultHelpModule)))); commandsPending = this._updateList.Select(x => x.Key).Distinct().ToList(); } else if (Configuration is not null && Configuration.EnableDefaultUserAppsHelp) { this._updateList.Add(new(null, new(typeof(DefaultUserAppsHelpModule)))); commandsPending = this._updateList.Select(x => x.Key).Distinct().ToList(); } else { try { this._updateList.Remove(new(null, new(typeof(DefaultHelpModule)))); } catch { } commandsPending = this._updateList.Select(x => x.Key).Distinct().ToList(); } if (globalCommands is not null && globalCommands.Count is not 0) GlobalDiscordCommands.AddRange(globalCommands); foreach (var key in commandsPending) { this.Client.Logger.Log(ApplicationCommandsLogLevel, key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands"); if (key.HasValue) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Found guild {guild} in shard {shard}!", key.Value, this.Client.ShardId); this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering"); } await this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value).ToList(), key).ConfigureAwait(false); } this.MISSING_SCOPE_GUILD_IDS = [..failedGuilds]; s_missingScopeGuildIdsGlobal.AddRange(failedGuilds); this.ShardStartupFinished = true; FinishedShardCount++; StartupFinished = FinishedShardCount == ShardCount; this.Client.Logger.Log(LogLevel.Information, "Application command setup finished for shard {ShardId}, enabling receiving", this.Client.ShardId); await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new(Configuration?.ServiceProvider) { RegisteredGlobalCommands = GlobalCommandsInternal, RegisteredGuildCommands = GuildCommandsInternal, GuildsWithoutScope = this.MISSING_SCOPE_GUILD_IDS, ShardId = this.Client.ShardId }).ConfigureAwait(false); this.FinishedRegistration(); } catch (Exception ex) { this.Client.Logger.LogCritical(ex, "There was an error during the application commands setup"); this.Client.Logger.LogError(ex.Message); this.Client.Logger.LogError(ex.StackTrace); } } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private async Task RegisterCommands(List types, ulong? guildId) { this.Client.Logger.Log(LogLevel.Information, "Registering commands on shard {shard}", this.Client.ShardId); //Initialize empty lists to be added to the global ones at the end var commandMethods = new List(); var groupCommands = new List(); var subGroupCommands = new List(); var contextMenuCommands = new List(); var updateList = new List(); var commandTypeSources = new List>(); var groupTranslation = new List(); var translation = new List(); List unitTestCommands = []; //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); var ctx = new ApplicationCommandsTranslationContext(type, module.FullName); config.Translations?.Invoke(ctx); //Add module to classes list if it's a group var extremeNestedGroup = false; if (module.GetCustomAttribute() is not null) classes.Add(module); else if (module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance).Any(x => x.IsDefined(typeof(SlashCommandGroupAttribute)))) { //Otherwise add the extreme nested groups classes = module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance) .Where(x => x.IsDefined(typeof(SlashCommandGroupAttribute))) .Select(x => module.GetNestedType(x.Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance).GetTypeInfo()).ToList(); extremeNestedGroup = true; } else //Otherwise add the nested groups classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); if (module.GetCustomAttribute() is not null || extremeNestedGroup) { List groupTranslations = null; if (!string.IsNullOrEmpty(ctx.GroupTranslations)) groupTranslations = JsonConvert.DeserializeObject>(ctx.GroupTranslations)!; var slashGroupsTuple = await NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations).ConfigureAwait(false); if (slashGroupsTuple.applicationCommands is not null && slashGroupsTuple.applicationCommands.Count is not 0) { if (IsCalledByUnitTest) unitTestCommands.AddRange(slashGroupsTuple.applicationCommands); updateList.AddRange(slashGroupsTuple.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cgwsgs = new List(); foreach (var cmd in slashGroupsTuple.applicationCommands) if (cmd.Type is ApplicationCommandType.ChatInput) { var cgs = new List(); var cs2 = new List(); if (cmd.Options is not null) { foreach (var scg in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommandGroup)) { var cs = new List(); if (scg.Options is not null) foreach (var sc in scg.Options) if (sc.Options is null || sc.Options.Count is 0) cs.Add(new(sc.Name, sc.Description, null, null, sc.RawNameLocalizations, sc.RawDescriptionLocalizations)); else cs.Add(new(sc.Name, sc.Description, [.. sc.Options], null, sc.RawNameLocalizations, sc.RawDescriptionLocalizations)); cgs.Add(new(scg.Name, scg.Description, cs, null, scg.RawNameLocalizations, scg.RawDescriptionLocalizations)); } foreach (var sc2 in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommand)) if (sc2.Options == null || sc2.Options.Count == 0) cs2.Add(new(sc2.Name, sc2.Description, null, null, sc2.RawNameLocalizations, sc2.RawDescriptionLocalizations)); else cs2.Add(new(sc2.Name, sc2.Description, [.. sc2.Options], null, sc2.RawNameLocalizations, sc2.RawDescriptionLocalizations)); } cgwsgs.Add(new(cmd.Name, cmd.Description, cgs, cs2, cmd.Type, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations)); } if (cgwsgs.Count is not 0) groupTranslation.AddRange(cgwsgs.Select(cgwsg => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cgwsg))!)); } } if (slashGroupsTuple.commandTypeSources is not null && slashGroupsTuple.commandTypeSources.Count is not 0) commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources); if (slashGroupsTuple.singletonModules is not null && slashGroupsTuple.singletonModules.Count is not 0) s_singletonModules.AddRange(slashGroupsTuple.singletonModules); if (slashGroupsTuple.groupCommands is not null && slashGroupsTuple.groupCommands.Count is not 0) groupCommands.AddRange(slashGroupsTuple.groupCommands); if (slashGroupsTuple.subGroupCommands is not null && slashGroupsTuple.subGroupCommands.Count is not 0) subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands); } //Handles methods and context menus, only if the module isn't a group itself if (module.GetCustomAttribute() is null) { List? commandTranslations = null; if (!string.IsNullOrEmpty(ctx.SingleTranslations)) commandTranslations = JsonConvert.DeserializeObject>(ctx.SingleTranslations); //Slash commands var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() is not null); var slashCommands = await CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations).ConfigureAwait(false); if (slashCommands.applicationCommands is not null && slashCommands.applicationCommands.Count is not 0) { if (IsCalledByUnitTest) unitTestCommands.AddRange(slashCommands.applicationCommands); updateList.AddRange(slashCommands.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cs = new List(); foreach (var cmd in slashCommands.applicationCommands.Where(cmd => cmd.Type is ApplicationCommandType.ChatInput && (cmd.Options is null || !cmd.Options.Any(x => x.Type is ApplicationCommandOptionType.SubCommand or ApplicationCommandOptionType.SubCommandGroup)))) if (cmd.Options == null || cmd.Options.Count == 0) cs.Add(new(cmd.Name, cmd.Description, null, ApplicationCommandType.ChatInput, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations)); else cs.Add(new(cmd.Name, cmd.Description, [.. cmd.Options], ApplicationCommandType.ChatInput, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations)); if (cs.Count is not 0) //translation.AddRange(cs.Select(c => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))!)); { foreach (var c in cs) { var json = JsonConvert.SerializeObject(c); var obj = JsonConvert.DeserializeObject(json); translation.Add(obj!); } } } } if (slashCommands.commandTypeSources is not null && slashCommands.commandTypeSources.Count is not 0) commandTypeSources.AddRange(slashCommands.commandTypeSources); if (slashCommands.commandMethods is not null && slashCommands.commandMethods.Count is not 0) commandMethods.AddRange(slashCommands.commandMethods); //Context Menus var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() is not null); var contextCommands = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations).ConfigureAwait(false); if (contextCommands.applicationCommands is not null && contextCommands.applicationCommands.Count is not 0) { if (IsCalledByUnitTest) unitTestCommands.AddRange(contextCommands.applicationCommands); updateList.AddRange(contextCommands.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cs = new List(); foreach (var cmd in contextCommands.applicationCommands) if (cmd.Type is ApplicationCommandType.Message or ApplicationCommandType.User) cs.Add(new(cmd.Name, null, null, cmd.Type)); if (cs.Count != 0) translation.AddRange(cs.Select(c => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))!)); } } if (contextCommands.commandTypeSources is not null && contextCommands.commandTypeSources.Count is not 0) commandTypeSources.AddRange(contextCommands.commandTypeSources); if (contextCommands.contextMenuCommands is not null && contextCommands.contextMenuCommands.Count is not 0) contextMenuCommands.AddRange(contextCommands.contextMenuCommands); //Accounts for lifespans if (module.GetCustomAttribute() is not null && module.GetCustomAttribute().Lifespan is ApplicationCommandModuleLifespan.Singleton) s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider)); } } catch (NullReferenceException ex) { this.Client.Logger.LogCritical(ex, "NRE Exception thrown: {msg}\nStack: {stack}", ex.Message, ex.StackTrace); } catch (Exception ex) { if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, @"There was an error registering application commands: {res}", brex.WebResponse.Response); else { if (ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, @"There was an error registering application commands: {res}", brex1.WebResponse.Response); else this.Client.Logger.LogCritical(ex, @"There was an error parsing the application commands"); } s_errored = true; } } if (!s_errored && !IsCalledByUnitTest) { updateList = updateList.DistinctBy(x => x.Name).ToList(); if (Configuration.GenerateTranslationFilesOnly) await this.CheckRegistrationStartup(translation, groupTranslation, guildId); else try { List commands = []; try { if (guildId is null) { if (updateList.Count is not 0) { var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(this.Client, updateList).ConfigureAwait(false); if (regCommands is not null) { var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GlobalCommandsInternal.AddRange(actualCommands); } } else if (GlobalDiscordCommands.Count is not 0) foreach (var cmd in GlobalDiscordCommands) try { await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id).ConfigureAwait(false); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete global command {cmdId}. Please clean up manually", cmd.Id); } } else { if (updateList.Count is not 0) { var regCommands = await RegistrationWorker.RegisterGuildCommandsAsync(this.Client, guildId.Value, updateList).ConfigureAwait(false); if (regCommands is not null) { var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GuildCommandsInternal.Add(guildId.Value, actualCommands); try { if (this.Client.Guilds.TryGetValue(guildId.Value, out var guild)) guild.InternalRegisteredApplicationCommands.AddRange(actualCommands); } catch (NullReferenceException) { } } } else if (GuildDiscordCommands.Count is not 0) foreach (var cmd in GuildDiscordCommands.First(x => x.Key == guildId.Value).Value) try { await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id).ConfigureAwait(false); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete guild command {cmdId} in guild {guildId}. Please clean up manually", cmd.Id, guildId.Value); } } } catch (UnauthorizedException ex) { this.Client.Logger.LogError("Could not register application commands for guild {guildId}.\nError: {exc}", guildId, ex.JsonMessage); return; } //Creates a guild command if a guild id is specified, otherwise global //Checks against the ids and adds them to the command method lists foreach (var command in commands) { if (commandMethods!.TryGetFirstValueWhere(x => x.Name == command.Name, out var com)) com.CommandId = command.Id; if (groupCommands!.TryGetFirstValueWhere(x => x.Name == command.Name, out var groupCom)) groupCom.CommandId = command.Id; if (subGroupCommands!.TryGetFirstValueWhere(x => x.Name == command.Name, out var subCom)) subCom.CommandId = command.Id; if (contextMenuCommands!.TryGetFirstValueWhere(x => x.Name == command.Name, out var cmCom)) cmCom.CommandId = command.Id; } //Adds to the global lists finally CommandMethods.AddRange(commandMethods.DistinctBy(x => x.Name)); GroupCommands.AddRange(groupCommands.DistinctBy(x => x.Name)); SubGroupCommands.AddRange(subGroupCommands.DistinctBy(x => x.Name)); ContextMenuCommands.AddRange(contextMenuCommands.DistinctBy(x => x.Name)); s_registeredCommands.Add(new(guildId, commands.ToList())); foreach (var app in commandMethods.Select(command => types.First(t => t.Type == command.Method.DeclaringType))) { } if (guildId.HasValue) await this._guildApplicationCommandsRegistered.InvokeAsync(this, new(Configuration?.ServiceProvider) { Handled = true, GuildId = guildId.Value, RegisteredCommands = GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value ?? [] }).ConfigureAwait(false); else await this._globalApplicationCommandsRegistered.InvokeAsync(this, new(Configuration?.ServiceProvider) { Handled = true, RegisteredCommands = GlobalCommandsInternal }).ConfigureAwait(false); await this.CheckRegistrationStartup(translation, groupTranslation, guildId); } catch (NullReferenceException ex) { this.Client.Logger.LogCritical(ex, "NRE Exception thrown: {msg}\nStack: {stack}", ex.Message, ex.StackTrace); } catch (Exception ex) { if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, @"There was an error registering application commands: {res}", brex.WebResponse.Response); else { if (ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, @"There was an error registering application commands: {res}", brex1.WebResponse.Response); else this.Client.Logger.LogCritical(ex, @"There was an general error registering application commands"); } s_errored = true; } } else if (IsCalledByUnitTest) { CommandMethods.AddRange(commandMethods.DistinctBy(x => x.Name)); GroupCommands.AddRange(groupCommands.DistinctBy(x => x.Name)); SubGroupCommands.AddRange(subGroupCommands.DistinctBy(x => x.Name)); ContextMenuCommands.AddRange(contextMenuCommands.DistinctBy(x => x.Name)); s_registeredCommands.Add(new(guildId, unitTestCommands.ToList())); foreach (var app in commandMethods.Select(command => types.First(t => t.Type == command.Method.DeclaringType))) { } if (guildId.HasValue) await this._guildApplicationCommandsRegistered.InvokeAsync(this, new(Configuration?.ServiceProvider) { Handled = true, GuildId = guildId.Value, RegisteredCommands = GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value ?? [] }).ConfigureAwait(false); else await this._globalApplicationCommandsRegistered.InvokeAsync(this, new(Configuration?.ServiceProvider) { Handled = true, RegisteredCommands = GlobalCommandsInternal }).ConfigureAwait(false); } } /// /// Checks the registration startup. /// /// The optional translations. /// The optional group translations. /// The optional guild id. private async Task CheckRegistrationStartup(List? translation = null, List? groupTranslation = null, ulong? guildId = null) { if (Configuration.GenerateTranslationFilesOnly) { try { if (translation is not null && translation.Count is not 0) { var fileName = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{(guildId.HasValue ? guildId.Value : "global")}.json"; var fs = File.Create(fileName); var ms = new MemoryStream(); var writer = new StreamWriter(ms); await writer.WriteAsync(JsonConvert.SerializeObject(translation.DistinctBy(x => x.Name), Formatting.Indented)).ConfigureAwait(false); await writer.FlushAsync().ConfigureAwait(false); ms.Position = 0; await ms.CopyToAsync(fs).ConfigureAwait(false); await fs.FlushAsync().ConfigureAwait(false); fs.Close(); await fs.DisposeAsync().ConfigureAwait(false); ms.Close(); await ms.DisposeAsync().ConfigureAwait(false); this.Client.Logger.LogInformation("Exported base translation to {exppath}", fileName); } if (groupTranslation is not null && groupTranslation.Count is not 0) { var fileName = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{(guildId.HasValue ? guildId.Value : "global")}.json"; var fs = File.Create(fileName); var ms = new MemoryStream(); var writer = new StreamWriter(ms); await writer.WriteAsync(JsonConvert.SerializeObject(groupTranslation.DistinctBy(x => x.Name), Formatting.Indented)).ConfigureAwait(false); await writer.FlushAsync().ConfigureAwait(false); ms.Position = 0; await ms.CopyToAsync(fs).ConfigureAwait(false); await fs.FlushAsync().ConfigureAwait(false); fs.Close(); await fs.DisposeAsync().ConfigureAwait(false); ms.Close(); await ms.DisposeAsync().ConfigureAwait(false); this.Client.Logger.LogInformation("Exported base translation to {exppath}", fileName); } } catch (Exception ex) { this.Client.Logger.LogError(@"{msg}", ex.Message); this.Client.Logger.LogError(@"{stack}", ex.StackTrace); } await this.Client.DisconnectAsync().ConfigureAwait(false); } } /// /// Checks whether we finished up the complete startup. /// private async Task CheckStartupFinishAsync(ApplicationCommandsExtension sender, ApplicationCommandsModuleStartupFinishedEventArgs args) { if (StartupFinished) if (!FinishFired) { await this._applicationCommandsModuleReady.InvokeAsync(sender, new(Configuration?.ServiceProvider) { GuildsWithoutScope = s_missingScopeGuildIdsGlobal }).ConfigureAwait(false); FinishFired = true; if (Configuration.GenerateTranslationFilesOnly) Environment.Exit(0); } args.Handled = false; } /// /// Interaction handler. /// /// The client. /// The event args. private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got slash interaction on shard {shard}", this.Client.ShardId); if (HandledInteractions.Contains(e.Interaction.Id) || (e.Interaction is { GuildId: not null, AuthorizingIntegrationOwners.GuildInstallKey: not null } && !client.Guilds.ContainsKey(e.Interaction.GuildId.Value))) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received or wrong shard"); return Task.FromResult(true); } HandledInteractions.Add(e.Interaction.Id); _ = Task.Run(async () => { var type = GetInteractionType(e.Interaction.Data); switch (e.Interaction.Type) { case InteractionType.ApplicationCommand: { //Creates the context var context = new InteractionContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Guild = e.Interaction.Guild, User = e.Interaction.User, Client = client, ApplicationCommandsExtension = this, CommandName = e.Interaction.Data.Name, InteractionId = e.Interaction.Id, Token = e.Interaction.Token, Services = Configuration?.ServiceProvider, ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList() ?? [], ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList() ?? [], ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList() ?? [], ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList() ?? [], Type = ApplicationCommandType.ChatInput, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions, Entitlements = e.Interaction.Entitlements, EntitlementSkuIds = e.Interaction.EntitlementSkuIds, UserId = e.Interaction.User.Id, GuildId = e.Interaction.GuildId, MemberId = e.Interaction.GuildId is not null ? e.Interaction.User.Id : null, ChannelId = e.Interaction.ChannelId }; try { if (s_errored) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup.").AsEphemeral()).ConfigureAwait(false); throw new InvalidOperationException("Application commands failed to register properly on startup."); } var methods = CommandMethods.Where(x => x.CommandId == e.Interaction.Data.Id).ToList(); var groups = GroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id).ToList(); var subgroups = SubGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id).ToList(); if (methods.Count is 0 && groups.Count is 0 && subgroups.Count is 0) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("An application command was executed, but no command was registered for it.").AsEphemeral()).ConfigureAwait(false); throw new InvalidOperationException($"An application command was executed, but no command was registered for it.\n\tCommand name: {e.Interaction.Data.Name}\n\tCommand ID: {e.Interaction.Data.Id}"); } switch (type) { case ApplicationCommandFinalType.Command when methods.Count is not 0: { var method = methods.First().Method; context.SubCommandName = null; context.SubSubCommandName = null; if (DebugEnabled) this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options).ConfigureAwait(false); await this.RunCommandAsync(context, method, args).ConfigureAwait(false); break; } case ApplicationCommandFinalType.SubCommand when groups.Count is not 0: { var command = e.Interaction.Data.Options[0]; var method = groups.First().Methods.First(x => x.Key == command.Name).Value; context.SubCommandName = command.Name; context.SubSubCommandName = null; if (DebugEnabled) this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options).ConfigureAwait(false); await this.RunCommandAsync(context, method, args).ConfigureAwait(false); break; } case ApplicationCommandFinalType.SubCommandGroup when subgroups.Count is not 0: { var command = e.Interaction.Data.Options[0]; var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var method = group.Methods.First(x => x.Key == command.Options[0].Name).Value; context.SubCommandName = command.Name; context.SubSubCommandName = command.Options[0].Name; if (DebugEnabled) this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options[0].Options).ConfigureAwait(false); await this.RunCommandAsync(context, method, args).ConfigureAwait(false); break; } case ApplicationCommandFinalType.NotDetermined: default: throw new ArgumentOutOfRangeException(null, "Could not determine application command type"); } await this._slashExecuted.InvokeAsync(this, new(this.Client.ServiceProvider) { Context = context }).ConfigureAwait(false); } catch (Exception ex) { await this._slashError.InvokeAsync(this, new(this.Client.ServiceProvider) { Context = context, Exception = ex }).ConfigureAwait(false); this.Client.Logger.LogError(ex, "Error in slash interaction"); } break; } case InteractionType.AutoComplete when s_errored: throw new InvalidOperationException("Application commands failed to register properly on startup."); case InteractionType.AutoComplete: { var methods = CommandMethods.Where(x => x.CommandId == e.Interaction.Data.Id).ToList(); var groups = GroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id).ToList(); var subgroups = SubGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id).ToList(); if (methods.Count is 0 && groups.Count is 0 && subgroups.Count is 0) throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it"); try { switch (type) { case ApplicationCommandFinalType.Command when methods.Count is not 0: { var focusedOption = e.Interaction.Data.Options.First(o => o.Focused); var method = methods.First().Method; var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Client = client, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = e.Interaction.Data.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions, Entitlements = e.Interaction.Entitlements, EntitlementSkuIds = e.Interaction.EntitlementSkuIds }; var choices = await ((Task>)providerMethod.Invoke(providerInstance, [context])).ConfigureAwait(false); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)).ConfigureAwait(false); break; } case ApplicationCommandFinalType.SubCommand when groups.Count is not 0: { var command = e.Interaction.Data.Options[0]; var group = groups.First().Methods.First(x => x.Key == command.Name).Value; var focusedOption = command.Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Client = client, Interaction = e.Interaction, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions, Entitlements = e.Interaction.Entitlements, EntitlementSkuIds = e.Interaction.EntitlementSkuIds }; var choices = await ((Task>)providerMethod.Invoke(providerInstance, [context])).ConfigureAwait(false); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)).ConfigureAwait(false); break; } case ApplicationCommandFinalType.SubCommandGroup when subgroups.Count is not 0: { var command = e.Interaction.Data.Options[0]; var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options[0].Name).Value; var focusedOption = command.Options[0].Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Client = client, Interaction = e.Interaction, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options[0].Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions, Entitlements = e.Interaction.Entitlements, EntitlementSkuIds = e.Interaction.EntitlementSkuIds }; var choices = await ((Task>)providerMethod.Invoke(providerInstance, [context])).ConfigureAwait(false); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)).ConfigureAwait(false); break; } } } catch (Exception ex) { this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); } break; } case InteractionType.Ping: case InteractionType.Component: case InteractionType.ModalSubmit: break; default: throw new ArgumentOutOfRangeException(null, "Received out unknown interaction type"); } }); return Task.CompletedTask; } /// /// Gets the interaction type from the interaction data. /// /// /// private static ApplicationCommandFinalType GetInteractionType(DiscordInteractionData data) { var type = ApplicationCommandFinalType.NotDetermined; if (data.Options.Count is 0) return ApplicationCommandFinalType.Command; if (data.Options.All(x => x.Type is not ApplicationCommandOptionType.SubCommand and not ApplicationCommandOptionType.SubCommandGroup)) return ApplicationCommandFinalType.Command; if (data.Options.Any(x => x.Type is ApplicationCommandOptionType.SubCommandGroup)) type = ApplicationCommandFinalType.SubCommandGroup; else if (data.Options.Any(x => x.Type is ApplicationCommandOptionType.SubCommand)) type = ApplicationCommandFinalType.SubCommand; return type; } /// /// Context menu handler. /// /// The client. /// The event args. private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got context menu interaction on shard {shard}", this.Client.ShardId); if (HandledInteractions.Contains(e.Interaction.Id)) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received"); return Task.FromResult(true); } HandledInteractions.Add(e.Interaction.Id); _ = Task.Run(async () => { //Creates the context var context = new ContextMenuContext(e.Type switch { ApplicationCommandType.User => DisCatSharpCommandType.UserCommand, ApplicationCommandType.Message => DisCatSharpCommandType.MessageCommand, _ => throw new ArgumentOutOfRangeException(nameof(e.Type), "Unknown context menu type") }) { Interaction = e.Interaction, Channel = e.Interaction.Channel, Client = client, Services = Configuration?.ServiceProvider, CommandName = e.Interaction.Data.Name, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, InteractionId = e.Interaction.Id, User = e.Interaction.User, Token = e.Interaction.Token, TargetUser = e.TargetUser, TargetMessage = e.TargetMessage, Type = e.Type, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions, Entitlements = e.Interaction.Entitlements, EntitlementSkuIds = e.Interaction.EntitlementSkuIds, UserId = e.Interaction.User.Id, GuildId = e.Interaction.GuildId, MemberId = e.Interaction.GuildId is not null ? e.Interaction.User.Id : null, ChannelId = e.Interaction.ChannelId }; try { if (s_errored) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Context menus failed to register properly on startup.").AsEphemeral()).ConfigureAwait(false); throw new InvalidOperationException("Context menus failed to register properly on startup."); } //Gets the method for the command var method = ContextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); if (method == null) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A context menu command was executed, but no command was registered for it.").AsEphemeral()).ConfigureAwait(false); throw new InvalidOperationException("A context menu command was executed, but no command was registered for it."); } await this.RunCommandAsync(context, method.Method, new[] { context }).ConfigureAwait(false); await this._contextMenuExecuted.InvokeAsync(this, new(this.Client.ServiceProvider) { Context = context }).ConfigureAwait(false); } catch (Exception ex) { await this._contextMenuErrored.InvokeAsync(this, new(this.Client.ServiceProvider) { Context = context, Exception = ex }).ConfigureAwait(false); } }); return Task.CompletedTask; } /// /// Runs a command. /// /// The base context. /// The method info. /// The arguments. internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Executing {cmd}", method.Name); //Accounts for lifespans var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient; var classInstance = moduleLifespan switch { ApplicationCommandModuleLifespan.Scoped => //Accounts for static methods and adds DI method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider), ApplicationCommandModuleLifespan.Transient => //Accounts for static methods and adds DI method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider), //If singleton, gets it from the singleton list ApplicationCommandModuleLifespan.Singleton => s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)), _ => throw new($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}") }; ApplicationCommandsModule module = null; if (classInstance is ApplicationCommandsModule mod) module = mod; switch (context) { // Slash commands case InteractionContext slashContext: { await RunPreexecutionChecksAsync(method, slashContext).ConfigureAwait(false); var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)).ConfigureAwait(false); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource).ConfigureAwait(false); await ((Task)method.Invoke(classInstance, args.ToArray())).ConfigureAwait(false); await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask).ConfigureAwait(false); } break; } // Context menus case ContextMenuContext contextMenuContext: { await RunPreexecutionChecksAsync(method, contextMenuContext).ConfigureAwait(false); var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)).ConfigureAwait(false); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource).ConfigureAwait(false); await ((Task)method.Invoke(classInstance, args.ToArray())).ConfigureAwait(false); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask).ConfigureAwait(false); } break; } } } /// /// Property injection /// /// The type. /// The services. internal static object CreateInstance(Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod is not null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && xf is { IsStatic: false, IsPublic: true }); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } /// /// Resolves the slash command parameters. /// /// The event arguments. /// The interaction context. /// The method info. /// The options. private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodBase method, IReadOnlyList options) { var args = new List { context }; var parameters = method.GetParameters().Skip(1).ToList(); foreach (var parameter in parameters) //Accounts for optional arguments without values given if (parameter.IsOptional && (options is null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute()?.Name.ToLower()) ?? true))) args.Add(parameter.DefaultValue); else { var option = options.Single(x => x.Name == parameter.GetCustomAttribute()?.Name.ToLower()); if (parameter.ParameterType == typeof(string)) args.Add(option.Value.ToString()); else if (parameter.ParameterType.IsEnum) args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); else if (parameter.ParameterType == typeof(ulong) || parameter.ParameterType == typeof(ulong?)) args.Add((ulong?)option.Value); else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) if (option.Value is null) args.Add(null); else args.Add(Convert.ToInt64(option.Value)); else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) args.Add((bool?)option.Value); else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) args.Add((double?)option.Value); else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(DiscordAttachment)) { //Checks through resolved if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); else args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord }); } else if (parameter.ParameterType == typeof(DiscordUser)) { //Checks through resolved if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else args.Add(await this.Client.GetUserAsync((ulong)option.Value).ConfigureAwait(false)); } else if (parameter.ParameterType == typeof(DiscordChannel)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); else args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordRole)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); } else if (parameter.ParameterType == typeof(SnowflakeObject)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else throw new ArgumentException("Error resolving mentionable option."); } else throw new ArgumentException("Error resolving interaction."); } return args; } /// /// Runs the pre-execution checks. /// /// The method info. /// The base context. private static async Task RunPreexecutionChecksAsync(MemberInfo method, BaseContext context) { switch (context) { case InteractionContext ctx: { //Gets all attributes from parent classes as well and stuff var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType!.GetCustomAttributes()); if (method.DeclaringType?.DeclaringType is not null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(ctx).ConfigureAwait(false); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value is false)) throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value is false).Select(x => x.Key).ToList() }; break; } case ContextMenuContext cMctx: { var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType!.GetCustomAttributes()); if (method.DeclaringType?.DeclaringType is not null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(cMctx).ConfigureAwait(false); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value is false)) throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value is false).Select(x => x.Key).ToList() }; break; } } } /// /// Gets the choice attributes from choice provider. /// /// The custom attributes. /// The optional guild id private static async Task> GetChoiceAttributesFromProvider(List customAttributes, ulong? guildId = null) { var choices = new List(); foreach (var choiceProviderAttribute in customAttributes) { var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider)); if (method is null) throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); else { var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType); // Abstract class offers more properties that can be set if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider))) { choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId)) ?.SetValue(instance, guildId); choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services)) ?.SetValue(instance, Configuration.ServiceProvider); } //Gets the choices from the method var result = (await ((Task>)method.Invoke(instance, null)!).ConfigureAwait(false)).ToList(); if (result.Count is not 0) choices.AddRange(result); } } return choices; } /// /// Gets the choice attributes from enum parameter. /// /// The enum parameter. private static List GetChoiceAttributesFromEnumParameter(Type enumParam) => (from Enum enumValue in Enum.GetValues(enumParam) select new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())).ToList(); /// /// Gets the parameter type. /// /// The type. private static ApplicationCommandOptionType GetParameterType(Type type) { var parameterType = type == typeof(string) ? ApplicationCommandOptionType.String : type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?) ? ApplicationCommandOptionType.Integer : type == typeof(bool) || type == typeof(bool?) ? ApplicationCommandOptionType.Boolean : type == typeof(double) || type == typeof(double?) ? ApplicationCommandOptionType.Number : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : type == typeof(DiscordChannel) ? ApplicationCommandOptionType.Channel : type == typeof(DiscordUser) ? ApplicationCommandOptionType.User : type == typeof(DiscordRole) ? ApplicationCommandOptionType.Role : type == typeof(SnowflakeObject) ? ApplicationCommandOptionType.Mentionable : type.IsEnum ? ApplicationCommandOptionType.String : throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parameterType; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. private static List GetChoiceAttributesFromParameter(List choiceAttributes) => choiceAttributes.Count is 0 ? [] : choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); /// /// Parses the parameters. /// /// The parameters. /// The command name. /// The optional guild id. internal static async Task> ParseParametersAsync(IEnumerable parameters, string commandName, ulong? guildId) { var options = new List(); foreach (var parameter in parameters) { //Gets the attribute var optionAttribute = parameter.GetCustomAttribute() ?? throw new ArgumentException($"One or more arguments of the command '{commandName}' are missing the Option attribute!"); var minimumValue = parameter.GetCustomAttribute()?.Value ?? null; var maximumValue = parameter.GetCustomAttribute()?.Value ?? null; var minimumLength = parameter.GetCustomAttribute()?.Value ?? null; var maximumLength = parameter.GetCustomAttribute()?.Value ?? null; var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; var autocompleteAttribute = parameter.GetCustomAttribute(); switch (optionAttribute.Autocomplete) { case true when autocompleteAttribute == null: throw new ArgumentException($"The command '{commandName}' has autocomplete enabled but is missing an autocomplete attribute!"); case false when autocompleteAttribute != null: throw new ArgumentException($"The command '{commandName}' has an autocomplete provider but the option to have autocomplete set to false!"); } //Sets the type var type = parameter.ParameterType; var parameterType = GetParameterType(type); switch (parameterType) { case ApplicationCommandOptionType.String: minimumValue = null; maximumValue = null; break; case ApplicationCommandOptionType.Integer: case ApplicationCommandOptionType.Number: minimumLength = null; maximumLength = null; break; case not ApplicationCommandOptionType.Channel: channelTypes = null; break; } //Handles choices //From attributes var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes().ToList()); //From enums if (parameter.ParameterType.IsEnum) choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); //From choice provider var choiceProviders = parameter.GetCustomAttributes().ToList(); if (choiceProviders.Count is not 0) choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId).ConfigureAwait(false); options.Add(new(optionAttribute.Name, optionAttribute.Description, parameterType, !parameter.IsOptional, choices, null, channelTypes, optionAttribute.Autocomplete, minimumValue, maximumValue, minimumLength: minimumLength, maximumLength: maximumLength)); } return options; } /* /// /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. /// public async Task RefreshCommandsAsync() { s_commandMethods.Clear(); s_groupCommands.Clear(); s_subGroupCommands.Clear(); _registeredCommands.Clear(); s_contextMenuCommands.Clear(); GlobalDiscordCommands.Clear(); GuildDiscordCommands.Clear(); GuildCommandsInternal.Clear(); GlobalCommandsInternal.Clear(); GlobalDiscordCommands = null; GuildDiscordCommands = null; s_errored = false; /*if (Configuration != null && Configuration.EnableDefaultHelp) { this._updateList.RemoveAll(x => x.Value.Type == typeof(DefaultHelpModule)); }*/ /* await this.UpdateAsync(); }*/ /// /// Fires when the execution of a slash command fails. /// public event AsyncEventHandler SlashCommandErrored { add => this._slashError.Register(value); remove => this._slashError.Unregister(value); } /// /// Fires the slash command error event. /// private readonly AsyncEvent _slashError; /// /// Fires when the execution of a slash command is successful. /// public event AsyncEventHandler SlashCommandExecuted { add => this._slashExecuted.Register(value); remove => this._slashExecuted.Unregister(value); } /// /// Fires the slash command executed event. /// private readonly AsyncEvent _slashExecuted; /// /// Fires when the execution of a context menu fails. /// public event AsyncEventHandler ContextMenuErrored { add => this._contextMenuErrored.Register(value); remove => this._contextMenuErrored.Unregister(value); } /// /// Fires the context menu error event. /// private readonly AsyncEvent _contextMenuErrored; /// /// Fire when the execution of a context menu is successful. /// public event AsyncEventHandler ContextMenuExecuted { add => this._contextMenuExecuted.Register(value); remove => this._contextMenuExecuted.Unregister(value); } /// /// Fires the context menu executed event. /// private readonly AsyncEvent _contextMenuExecuted; } /// /// Holds configuration data for setting up an application command. /// internal sealed class ApplicationCommandsModuleConfiguration { /// /// The type of the command module. /// public Type Type { get; } /// /// The translation setup. /// public Action? Translations { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The translation setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action? translations = null) { this.Type = type; this.Translations = translations; } } /// /// Links a command to its original command module. /// internal class ApplicationCommandSourceLink { /// /// The command. /// public DiscordApplicationCommand ApplicationCommand { get; set; } /// /// The base/root module the command is contained in. /// public Type RootCommandContainerType { get; set; } /// /// The direct group the command is contained in. /// public Type CommandContainerType { get; set; } } /// /// The command method. /// internal sealed class CommandMethod { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; init; } /// /// Gets or sets the method. /// public MethodInfo Method { get; init; } } /// /// The group command. /// internal sealed class GroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; init; } /// /// Gets or sets the methods. /// public List> Methods { get; init; } = []; } /// /// The sub group command. /// internal sealed class SubGroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; init; } /// /// Gets or sets the sub commands. /// public List SubCommands { get; set; } = []; } /// /// The context menu command. /// internal sealed class ContextMenuCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; init; } /// /// Gets or sets the method. /// public MethodInfo Method { get; init; } } #region Default Help /// /// Represents the default help module. /// internal sealed class DefaultHelpModule : ApplicationCommandsModule { public sealed class DefaultHelpAutoCompleteProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } else { await Task.WhenAll(globalCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } var options = slashCommands.Take(25).Select(sc => new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim())).ToList(); return options.AsEnumerable(); } } public sealed class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (command?.Options is null) options.Add(new("no_options_for_this_command", "no_options_for_this_command")); else { var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand && c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList(); options.AddRange(opt.Take(25).Select(option => new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()))); } return options.AsEnumerable(); } } public sealed class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (command.Options is null) { options.Add(new("no_options_for_this_command", "no_options_for_this_command")); return options.AsEnumerable(); } var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (foundCommand?.Options is null) options.Add(new("no_options_for_this_command", "no_options_for_this_command")); else { var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand && x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList(); options.AddRange(opt.Take(25).Select(option => new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()))); } return options.AsEnumerable(); } } [SlashCommand("help", "Displays command help")] internal async Task DefaultHelpAsync( InteractionContext ctx, [Autocomplete(typeof(DefaultHelpAutoCompleteProvider)), Option("option_one", "top level command to provide help for", true)] string commandName, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider)), Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider)), Option("option_three", "command to provide help for", true)] string commandTwoName = null ) { List applicationCommands = null; var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync(); if (ctx.Guild != null) { var guildCommandsTask = ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask).ConfigureAwait(false); applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); } else { await Task.WhenAll(globalCommandsTask).ConfigureAwait(false); applicationCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); } if (applicationCommands.Count < 1) { if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .WithContent($"There are no slash commands")).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"There are no slash commands").AsEphemeral()).ConfigureAwait(false); return; } if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command")) { var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup)); var subCommandParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options .FirstOrDefault(opt => opt.Name.Equals(commandOneName, StringComparison.OrdinalIgnoreCase)); var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName, StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {cmdParent.Name} {cmd.Name}")}: {cmd.Description ?? "No description provided."}" }; if (cmd.Options is not null) { var commandOptions = cmd.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .AddEmbed(discordEmbed)).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false); } else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command")) { var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand)); var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName, StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {subCommand.Name}")}: {subCommand.Description ?? "No description provided."}" }; if (subCommand.Options is not null) { var commandOptions = subCommand.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false); } else { var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); if (command is null) { if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}")).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral()).ConfigureAwait(false); return; } var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{command.Mention}: {command.Description ?? "No description provided."}" }.AddField(new("Command is NSFW", command.IsNsfw.ToString())); if (command.Options is not null) { var commandOptions = command.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false); } } } #endregion #region Default User Apps Help /// /// Represents the default user apps help module. /// internal sealed class DefaultUserAppsHelpModule : ApplicationCommandsModule { public sealed class DefaultUserAppsHelpAutoCompleteProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } else { await globalCommandsTask.ConfigureAwait(false); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } var options = slashCommands.Take(25).Select(sc => new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim())).ToList(); return options.AsEnumerable(); } } public sealed class DefaultUserAppsHelpAutoCompleteLevelOneProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await globalCommandsTask.ConfigureAwait(false); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (command?.Options is null) options.Add(new("no_options_for_this_command", "no_options_for_this_command")); else { var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand && c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList(); options.AddRange(opt.Take(25).Select(option => new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()))); } return options.AsEnumerable(); } } public sealed class DefaultUserAppsHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask).ConfigureAwait(false); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await globalCommandsTask.ConfigureAwait(false); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (command.Options is null) { options.Add(new("no_options_for_this_command", "no_options_for_this_command")); return options.AsEnumerable(); } var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (foundCommand?.Options is null) options.Add(new("no_options_for_this_command", "no_options_for_this_command")); else { var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand && x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList(); options.AddRange(opt.Take(25).Select(option => new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()))); } return options.AsEnumerable(); } } - [SlashCommand("help", "Displays command help", false, [ApplicationCommandContexts.Guild, ApplicationCommandContexts.BotDm, ApplicationCommandContexts.PrivateChannel], [ApplicationCommandIntegrationTypes.GuildInstall, ApplicationCommandIntegrationTypes.UserInstall])] + [SlashCommand("help", "Displays command help", false, [InteractionContextType.Guild, InteractionContextType.BotDm, InteractionContextType.PrivateChannel], [ApplicationCommandIntegrationTypes.GuildInstall, ApplicationCommandIntegrationTypes.UserInstall])] internal async Task DefaulUserAppstHelpAsync( InteractionContext ctx, [Autocomplete(typeof(DefaultUserAppsHelpAutoCompleteProvider)), Option("option_one", "top level command to provide help for", true)] string commandName, [Autocomplete(typeof(DefaultUserAppsHelpAutoCompleteLevelOneProvider)), Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null, [Autocomplete(typeof(DefaultUserAppsHelpAutoCompleteLevelTwoProvider)), Option("option_three", "command to provide help for", true)] string commandTwoName = null ) { List applicationCommands = null; var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync(); if (ctx.Guild != null) { var guildCommandsTask = ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask).ConfigureAwait(false); applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); } else { await globalCommandsTask.ConfigureAwait(false); applicationCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); } if (applicationCommands.Count < 1) { if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .WithContent($"There are no slash commands")).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"There are no slash commands").AsEphemeral()).ConfigureAwait(false); return; } if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command")) { var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup)); var subCommandParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options .FirstOrDefault(opt => opt.Name.Equals(commandOneName, StringComparison.OrdinalIgnoreCase)); var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName, StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {cmdParent.Name} {cmd.Name}")}: {cmd.Description ?? "No description provided."}" }; if (cmd.Options is not null) { var commandOptions = cmd.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .AddEmbed(discordEmbed)).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false); } else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command")) { var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand)); var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName, StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {subCommand.Name}")}: {subCommand.Description ?? "No description provided."}" }; if (subCommand.Options is not null) { var commandOptions = subCommand.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false); } else { var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); if (command is null) { if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}")).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral()).ConfigureAwait(false); return; } var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{command.Mention}: {command.Description ?? "No description provided."}" }.AddField(new("Command is NSFW", command.IsNsfw.ToString())); if (command.Options is not null) { var commandOptions = command.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)).ConfigureAwait(false); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false); } } } #endregion diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs index 5632a4306..04e827421 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs @@ -1,141 +1,141 @@ using System; using System.Collections.Generic; using System.Linq; using DisCatSharp.Enums; namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Represents a with the type of or . /// [AttributeUsage(AttributeTargets.Method)] public sealed class ContextMenuAttribute : Attribute { /// /// Gets the name of this context menu. /// public string Name { get; internal set; } /// /// Gets the type of this context menu. /// public ApplicationCommandType Type { get; internal set; } /// /// Gets this context menu's needed permissions. /// public Permissions? DefaultMemberPermissions { get; internal set; } /// /// Gets the allowed contexts of this context menu. /// - public List? AllowedContexts { get; set; } + public List? AllowedContexts { get; set; } /// /// Gets the allowed integration types of this context menu. /// public List? IntegrationTypes { get; set; } /// /// Gets whether this context menu can be used in direct messages. /// public bool? DmPermission { get; set; } /// /// Gets whether this context menu is marked as NSFW. /// public bool IsNsfw { get; set; } /// /// Marks this method as a context menu. /// /// The type of the context menu. /// The name of the context menu. /// Whether the context menu is marked as NSFW. /// The allowed contexts of the context menu. /// The allowed integration types of the context menu. - public ContextMenuAttribute(ApplicationCommandType type, string name, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public ContextMenuAttribute(ApplicationCommandType type, string name, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { if (type == ApplicationCommandType.ChatInput) throw new ArgumentException("Context menus cannot be of type ChatInput (Slash)."); this.Type = type; this.Name = name; this.DefaultMemberPermissions = null; this.DmPermission = null; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as a context menu. /// /// The type of the context menu. /// The name of the context menu. /// The default member permissions of the context menu. /// Whether the context menu is marked as NSFW. /// The allowed contexts of the context menu. /// The allowed integration types of the context menu. - public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { if (type == ApplicationCommandType.ChatInput) throw new ArgumentException("Context menus cannot be of type ChatInput (Slash)."); this.Type = type; this.Name = name; this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions; this.DmPermission = null; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as context menu. /// /// The type of the context menu. /// The name of the context menu. /// The dm permission of the context menu. /// Whether the context menu is marked as NSFW. /// The allowed contexts of the context menu. /// The allowed integration types of the context menu. - public ContextMenuAttribute(ApplicationCommandType type, string name, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public ContextMenuAttribute(ApplicationCommandType type, string name, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { if (type == ApplicationCommandType.ChatInput) throw new ArgumentException("Context menus cannot be of type ChatInput (Slash)."); this.Type = type; this.Name = name; this.DefaultMemberPermissions = null; this.DmPermission = dmPermission; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as a context menu. /// /// The type of the context menu. /// The name of the context menu. /// The default member permissions of the context menu. /// The dm permission of the context menu. /// Whether the context menu is marked as NSFW. /// The allowed contexts of the context menu. /// The allowed integration types of the context menu. - public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { if (type == ApplicationCommandType.ChatInput) throw new ArgumentException("Context menus cannot be of type ChatInput (Slash)."); this.Type = type; this.Name = name; this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions; this.DmPermission = dmPermission; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } } diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs index 7dec292a2..56633f0c9 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs @@ -1,129 +1,129 @@ using System; using System.Collections.Generic; using System.Linq; using DisCatSharp.Enums; namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Represents a . /// [AttributeUsage(AttributeTargets.Method)] public class SlashCommandAttribute : Attribute { /// /// Gets the name of this command. /// public string Name { get; set; } /// /// Gets the description of this command. /// public string Description { get; set; } /// /// Gets the needed permission of this command. /// public Permissions? DefaultMemberPermissions { get; set; } /// /// Gets the allowed contexts of this command. /// - public List? AllowedContexts { get; set; } + public List? AllowedContexts { get; set; } /// /// Gets the allowed integration types of this command. /// public List? IntegrationTypes { get; set; } /// /// Gets the dm permission of this command. /// public bool? DmPermission { get; set; } /// /// Gets whether this command is marked as NSFW. /// public bool IsNsfw { get; set; } /// /// Marks this method as a slash command. /// /// The name of this slash command. /// The description of this slash command. /// Whether this command is marked as NSFW. /// The allowed contexts of this slash command. /// The allowed integration types. - public SlashCommandAttribute(string name, string description, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public SlashCommandAttribute(string name, string description, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { this.Name = name.ToLower(); this.Description = description; this.DefaultMemberPermissions = null; this.DmPermission = null; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as a slash command. /// /// The name of this slash command. /// The description of this slash command. /// The default member permissions. /// Whether this command is marked as NSFW. /// The allowed contexts of this slash command. /// The allowed integration types. - public SlashCommandAttribute(string name, string description, long defaultMemberPermissions, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public SlashCommandAttribute(string name, string description, long defaultMemberPermissions, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { this.Name = name.ToLower(); this.Description = description; this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions; this.DmPermission = null; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as a slash command. /// /// The name of this slash command. /// The description of this slash command. /// The dm permission. /// Whether this command is marked as NSFW. /// The allowed contexts of this slash command. /// The allowed integration types. - public SlashCommandAttribute(string name, string description, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public SlashCommandAttribute(string name, string description, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { this.Name = name.ToLower(); this.Description = description; this.DefaultMemberPermissions = null; this.DmPermission = dmPermission; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as a slash command. /// /// The name of this slash command. /// The description of this slash command. /// The default member permissions. /// The dm permission. /// Whether this command is marked as NSFW. /// The allowed contexts of this slash command. /// The allowed integration types. - public SlashCommandAttribute(string name, string description, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public SlashCommandAttribute(string name, string description, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { this.Name = name.ToLower(); this.Description = description; this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions; this.DmPermission = dmPermission; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } } diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs index c0ac7557c..a13f89de5 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs @@ -1,129 +1,129 @@ using System; using System.Collections.Generic; using System.Linq; using DisCatSharp.Enums; namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Represents a group. /// [AttributeUsage(AttributeTargets.Class)] public class SlashCommandGroupAttribute : Attribute { /// /// Gets the name of this slash command group. /// public string Name { get; set; } /// /// Gets the description of this slash command group. /// public string Description { get; set; } /// /// Gets the needed permission of this slash command group. /// public Permissions? DefaultMemberPermissions { get; set; } /// /// Gets the allowed contexts of this slash command group. /// - public List? AllowedContexts { get; set; } + public List? AllowedContexts { get; set; } /// /// Gets the allowed integration types of this slash command group. /// public List? IntegrationTypes { get; set; } /// /// Gets the dm permission of this slash command group. /// public bool? DmPermission { get; set; } /// /// Gets whether this slash command group is marked as NSFW. /// public bool IsNsfw { get; set; } /// /// Marks this class as a slash command group. /// /// The name of the slash command group. /// The description of the slash command group. /// Whether the slash command group is marked as NSFW. /// The allowed contexts of the slash command group. /// The allowed integration types of the slash command group. - public SlashCommandGroupAttribute(string name, string description, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public SlashCommandGroupAttribute(string name, string description, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { this.Name = name.ToLower(); this.Description = description; this.DefaultMemberPermissions = null; this.DmPermission = null; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as a slash command group. /// /// The name of the slash command group. /// The description of the slash command group. /// The default member permissions of the slash command group. /// Whether the slash command group is marked as NSFW. /// The allowed contexts of the slash command group. /// The allowed integration types of the slash command group. - public SlashCommandGroupAttribute(string name, string description, long defaultMemberPermissions, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public SlashCommandGroupAttribute(string name, string description, long defaultMemberPermissions, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { this.Name = name.ToLower(); this.Description = description; this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions; this.DmPermission = null; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as a slash command group. /// /// The name of the slash command group. /// The description of the slash command group. /// The dm permission of the slash command group. /// Whether the slash command group is marked as NSFW. /// The allowed contexts of the slash command group. /// The allowed integration types of the slash command group. - public SlashCommandGroupAttribute(string name, string description, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public SlashCommandGroupAttribute(string name, string description, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { this.Name = name.ToLower(); this.Description = description; this.DefaultMemberPermissions = null; this.DmPermission = dmPermission; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } /// /// Marks this method as a slash command group. /// /// The name of the slash command group. /// The description of the slash command group. /// The default member permissions of the slash command group. /// The dm permission of the slash command group. /// Whether the slash command group is marked as NSFW. /// The allowed contexts of the slash command group. /// The allowed integration types of the slash command group. - public SlashCommandGroupAttribute(string name, string description, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) + public SlashCommandGroupAttribute(string name, string description, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null) { this.Name = name.ToLower(); this.Description = description; this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions; this.DmPermission = dmPermission; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts?.ToList(); this.IntegrationTypes = integrationTypes?.ToList(); } } diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs index 5ca8cfdd4..c03874269 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs @@ -1,231 +1,231 @@ using System; using System.Collections.Generic; using System.Linq; using DisCatSharp.Attributes; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a command that is registered to an application. /// public class DiscordApplicationCommand : SnowflakeObject, IEquatable { /// /// Gets the type of this application command. /// [JsonProperty("type")] public ApplicationCommandType Type { get; internal set; } /// /// Gets the unique ID of this command's application. /// [JsonProperty("application_id")] public ulong ApplicationId { get; internal set; } /// /// Gets the name of this command. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Sets the name localizations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] internal Dictionary? RawNameLocalizations { get; set; } /// /// Gets the name localizations. /// [JsonIgnore] public DiscordApplicationCommandLocalization? NameLocalizations => this.RawNameLocalizations != null ? new(this.RawNameLocalizations) : null; /// /// Gets the description of this command. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string? Description { get; internal set; } /// /// Sets the description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] internal Dictionary? RawDescriptionLocalizations { get; set; } /// /// Gets the description localizations. /// [JsonIgnore] public DiscordApplicationCommandLocalization? DescriptionLocalizations => this.RawDescriptionLocalizations != null ? new(this.RawDescriptionLocalizations) : null; /// /// Gets the potential parameters for this command. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public List? Options { get; internal set; } = null; /// /// Gets the commands needed permissions. /// [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions? DefaultMemberPermissions { get; internal set; } = null; /// /// Gets whether the command can be used in direct messages. /// [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] public bool? DmPermission { get; internal set; } /// /// Gets where the application command can be used. /// [JsonProperty("contexts", NullValueHandling = NullValueHandling.Ignore), DiscordUnreleased] - public List? AllowedContexts { get; internal set; } + public List? AllowedContexts { get; internal set; } /// /// Gets the application command allowed integration types. /// [JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore), DiscordUnreleased] public List? IntegrationTypes { get; internal set; } /// /// Gets whether the command is marked as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public bool IsNsfw { get; internal set; } = false; /// /// Gets the version number for this command. /// [JsonProperty("version")] public ulong Version { get; internal set; } /// /// Gets the mention for this command. /// [JsonIgnore] public string Mention => this.Type == ApplicationCommandType.ChatInput ? $"" : this.Name; /// /// Creates a new instance of a . /// /// The name of the command. /// The description of the command. /// Optional parameters for this command. /// The type of the command. Defaults to ChatInput. /// The localizations of the command name. /// The localizations of the command description. /// The default member permissions. /// The dm permission. /// Whether this command is NSFW. /// Where the command can be used. /// The allowed integration types. public DiscordApplicationCommand( string name, string? description, IEnumerable? options = null, ApplicationCommandType type = ApplicationCommandType.ChatInput, DiscordApplicationCommandLocalization? nameLocalizations = null, DiscordApplicationCommandLocalization? descriptionLocalizations = null, Permissions? defaultMemberPermissions = null, bool? dmPermission = null, bool isNsfw = false, - List? allowedContexts = null, + List? allowedContexts = null, List? integrationTypes = null ) : base(["guild_id", "name_localizations", "description_localizations"]) { if (type is ApplicationCommandType.ChatInput) { if (!Utilities.IsValidSlashCommandName(name)) throw new ArgumentException("Invalid slash command name specified. It must be below 32 characters and not contain any whitespace.", nameof(name)); if (name.Any(char.IsUpper)) throw new ArgumentException("Slash command name cannot have any upper case characters.", nameof(name)); if (description?.Length > 100) throw new ArgumentException("Slash command description cannot exceed 100 characters.", nameof(description)); if (string.IsNullOrWhiteSpace(description)) throw new ArgumentException("Slash commands need a description.", nameof(description)); this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs(); this.RawDescriptionLocalizations = descriptionLocalizations?.GetKeyValuePairs(); } else { if (!string.IsNullOrWhiteSpace(description)) throw new ArgumentException("Context menus do not support descriptions."); if (options?.Any() ?? false) throw new ArgumentException("Context menus do not support options."); description = string.Empty; this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs(); } var optionsList = options != null && options.Any() ? options.ToList() : null; this.Type = type; this.Name = name; this.Description = description; this.Options = optionsList; this.DefaultMemberPermissions = defaultMemberPermissions; this.DmPermission = dmPermission; this.IsNsfw = isNsfw; this.AllowedContexts = allowedContexts; this.IntegrationTypes = integrationTypes; } /// /// Creates a new empty Discord Application Command. /// internal DiscordApplicationCommand() : base(["name_localizations", "description_localizations", "guild_id"]) // Why tf is that so inconsistent?! { } /// /// Checks whether this object is equal to another object. /// /// The command to compare to. /// Whether the command is equal to this . public bool Equals(DiscordApplicationCommand other) => this.Id == other.Id; /// /// Determines if two objects are equal. /// /// The first command object. /// The second command object. /// Whether the two objects are equal. public static bool operator ==(DiscordApplicationCommand e1, DiscordApplicationCommand e2) => e1.Equals(e2); /// /// Determines if two objects are not equal. /// /// The first command object. /// The second command object. /// Whether the two objects are not equal. public static bool operator !=(DiscordApplicationCommand e1, DiscordApplicationCommand e2) => !(e1 == e2); /// /// Determines if a is equal to the current . /// /// The object to compare to. /// Whether the two objects are not equal. public override bool Equals(object other) => other is DiscordApplicationCommand dac && this.Equals(dac); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs index e5d3c1fb1..00a8c37fd 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs @@ -1,253 +1,253 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using DisCatSharp.Attributes; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents an interaction that was invoked. /// public sealed class DiscordInteraction : SnowflakeObject { /// /// Gets the type of interaction invoked. /// [JsonProperty("type")] public InteractionType Type { get; internal set; } /// /// Gets the command data for this interaction. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionData Data { get; internal set; } /// /// Gets the Id of the guild that invoked this interaction, if any. /// [JsonIgnore] public ulong? GuildId { get; internal set; } /// /// Gets the guild that invoked this interaction. /// [JsonIgnore] // TODO: Is now also "guild" public DiscordGuild Guild => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); /// /// Gets the Id of the channel that invoked this interaction. /// [JsonIgnore] public ulong ChannelId { get; internal set; } /// /// Gets the channel that invoked this interaction. /// [JsonIgnore] // TODO: Is now also partial "channel" public DiscordChannel Channel => (this.Discord as DiscordClient).InternalGetCachedChannel(this.ChannelId) ?? (DiscordChannel)(this.Discord as DiscordClient).InternalGetCachedThread(this.ChannelId) ?? (this.Guild == null ? new DiscordDmChannel { Id = this.ChannelId, Type = ChannelType.Private, Discord = this.Discord } : new DiscordChannel() { Id = this.ChannelId, Discord = this.Discord }); /// /// Gets the user that invoked this interaction. /// This can be cast to a if created in a guild. /// [JsonIgnore] public DiscordUser User { get; internal set; } /// /// Gets the continuation token for responding to this interaction. /// [JsonProperty("token")] public string Token { get; internal set; } /// /// Gets the version number for this interaction type. /// [JsonProperty("version")] public int Version { get; internal set; } /// /// Gets the ID of the application that created this interaction. /// [JsonProperty("application_id")] public ulong ApplicationId { get; internal set; } /// /// The message this interaction was created with, if any. /// [JsonProperty("message")] internal DiscordMessage Message { get; set; } /// /// Gets the invoking user locale. /// [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public string Locale { get; internal set; } /// /// Gets the guild locale if applicable. /// [JsonProperty("guild_locale", NullValueHandling = NullValueHandling.Ignore)] public string GuildLocale { get; internal set; } /// /// Gets the applications permissions. /// [JsonProperty("app_permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions AppPermissions { get; internal set; } /// /// Gets the entitlements. /// This is related to premium subscriptions for bots. /// Can only be used if you have an associated application subscription sku. /// [JsonProperty("entitlements", NullValueHandling = NullValueHandling.Ignore), DiscordInExperiment("Currently in closed beta."), Experimental("We provide this type but can't provide support.")] public List Entitlements { get; internal set; } = []; /// /// Gets the entitlement sku ids. /// This is related to premium subscriptions for bots. /// Can only be used if you have an associated application subscription sku. /// [JsonProperty("entitlement_sku_ids", NullValueHandling = NullValueHandling.Ignore), DiscordInExperiment("Currently in closed beta."), Experimental("We provide this type but can't provide support.")] public List EntitlementSkuIds { get; internal set; } = []; /// /// Gets which integrations authorized the interaction. /// [JsonProperty("authorizing_integration_owners", NullValueHandling = NullValueHandling.Ignore)] public AuthorizingIntegrationOwners? AuthorizingIntegrationOwners { get; internal set; } /// /// Gets the interaction's calling context. /// [JsonProperty("context", NullValueHandling = NullValueHandling.Ignore)] - public ApplicationCommandContexts Context { get; internal set; } + public InteractionContextType Context { get; internal set; } /// /// Creates a response to this interaction. /// /// 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); /// /// Creates a modal response to this interaction. /// /// The data to send. public Task CreateInteractionModalResponseAsync(DiscordInteractionModalBuilder builder) => this.Type != InteractionType.Ping && this.Type != InteractionType.ModalSubmit ? this.Discord.ApiClient.CreateInteractionModalResponseAsync(this.Id, this.Token, InteractionResponseType.Modal, builder) : throw new NotSupportedException("You can't respond to a PING with a modal."); // TODO: Add hints support /// /// Creates an iframe response to this interaction. /// /// The custom id of the iframe. /// The title of the iframe. /// The size of the iframe. /// The path of the iframe. Uses %application_id%.discordsays.com/:iframe_path. public Task CreateInteractionIframeResponseAsync(string customId, string title, IframeModalSize modalSize = IframeModalSize.Normal, string? iFramePath = null) => this.Type != InteractionType.Ping ? this.Discord.ApiClient.CreateInteractionIframeResponseAsync(this.Id, this.Token, InteractionResponseType.Iframe, customId, title, modalSize, iFramePath) : throw new NotSupportedException("You can't respond to a PING with an iframe."); /// /// Gets the original interaction response. /// /// The original message that was sent. This does not work on ephemeral messages. public Task GetOriginalResponseAsync() => this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); /// /// Edits the original interaction response. /// /// The webhook builder. /// The edited . public async Task EditOriginalResponseAsync(DiscordWebhookBuilder builder) { builder.Validate(isInteractionResponse: true); if (builder.KeepAttachmentsInternal.HasValue && builder.KeepAttachmentsInternal.Value) { var attachments = this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token).Result.Attachments; if (attachments?.Count > 0) builder.AttachmentsInternal.AddRange(attachments); } else if (builder.KeepAttachmentsInternal.HasValue) builder.AttachmentsInternal.Clear(); return await this.Discord.ApiClient.EditOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false); } /// /// Deletes the original interaction response. /// > public Task DeleteOriginalResponseAsync() => this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); /// /// Creates a follow up message to this interaction. /// /// The webhook builder. /// The created . public async Task CreateFollowupMessageAsync(DiscordFollowupMessageBuilder builder) { builder.Validate(); return await this.Discord.ApiClient.CreateFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false); } /// /// 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); /// /// Edits a follow up message. /// /// The id of the follow up message. /// The webhook builder. /// The edited . public async Task EditFollowupMessageAsync(ulong messageId, DiscordWebhookBuilder builder) { builder.Validate(isFollowup: true); if (builder.KeepAttachmentsInternal.HasValue && builder.KeepAttachmentsInternal.Value) { var attachments = this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId).Result.Attachments; if (attachments?.Count > 0) builder.AttachmentsInternal.AddRange(attachments); } else if (builder.KeepAttachmentsInternal.HasValue) builder.AttachmentsInternal.Clear(); return await this.Discord.ApiClient.EditFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId, builder).ConfigureAwait(false); } /// /// 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); internal DiscordInteraction() : base(["member", "guild_id", "channel_id", "channel", "guild", "user"]) { } } diff --git a/DisCatSharp/Enums/Application/ApplicationCommandContexts.cs b/DisCatSharp/Enums/Interaction/InteractionContextType.cs similarity index 79% rename from DisCatSharp/Enums/Application/ApplicationCommandContexts.cs rename to DisCatSharp/Enums/Interaction/InteractionContextType.cs index 20052cd08..03aae9926 100644 --- a/DisCatSharp/Enums/Application/ApplicationCommandContexts.cs +++ b/DisCatSharp/Enums/Interaction/InteractionContextType.cs @@ -1,22 +1,22 @@ namespace DisCatSharp.Enums; /// -/// Represents where application commands can be used. +/// Represents the interaction context type. /// -public enum ApplicationCommandContexts +public enum InteractionContextType { /// /// Command can be used in guilds. /// Guild = 0, /// /// Command can be used in direct messages with the bot. /// BotDm = 1, /// /// Command can be used in group direct messages and direct messages. /// PrivateChannel = 2 } diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index 2c5d079cc..3e93f0eb6 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -1,323 +1,323 @@ using System.Collections.Generic; using DisCatSharp.Entities; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions; /// /// Represents a application command create payload. /// internal sealed class RestApplicationCommandCreatePayload : ObservableApiObject { /// /// Gets the type. /// [JsonProperty("type")] public ApplicationCommandType Type { get; set; } /// /// Gets the name. /// [JsonProperty("name")] public string Name { get; set; } /// /// Gets the name localizations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] public Optional?> NameLocalizations { get; set; } /// /// Gets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string? Description { get; set; } /// /// Gets the description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] public Optional?> DescriptionLocalizations { get; set; } /// /// Gets the options. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable? Options { get; set; } /// /// Whether the command is allowed for everyone. /// [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Include)] public bool? DefaultPermission { get; set; } = null; /// /// The command needed permissions. /// [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Include)] public Permissions? DefaultMemberPermission { get; set; } /// /// Whether the command is allowed for dms. /// [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Include)] public bool? DmPermission { get; set; } /// /// Whether the command is marked as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public bool Nsfw { get; set; } /// /// Gets where the command is allowed at. /// [JsonProperty("contexts", NullValueHandling = NullValueHandling.Include)] - public List? AllowedContexts { get; set; } + public List? AllowedContexts { get; set; } /// /// Gets the allowed integration types. /// [JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore)] public List? IntegrationTypes { get; set; } } /// /// Represents a application command edit payload. /// internal sealed class RestApplicationCommandEditPayload : ObservableApiObject { /// /// Gets the name. /// [JsonProperty("name")] public Optional Name { get; set; } /// /// Gets the name localizations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] public Optional?> NameLocalizations { get; set; } /// /// Gets the description. /// [JsonProperty("description")] public Optional Description { get; set; } /// /// Gets the description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] public Optional?> DescriptionLocalizations { get; set; } /// /// Gets the options. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public Optional?> Options { get; set; } /// /// The command needed permissions. /// [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Include)] public Optional DefaultMemberPermission { get; set; } /// /// Whether the command is allowed for dms. /// [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Include)] public Optional DmPermission { get; set; } /// /// Whether the command is marked as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public Optional Nsfw { get; set; } /// /// Gets where the command is allowed at. /// [JsonProperty("contexts", NullValueHandling = NullValueHandling.Include)] - public Optional?> AllowedContexts { get; set; } + public Optional?> AllowedContexts { get; set; } /// /// Gets the allowed integration types. /// [JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore)] public Optional?> IntegrationTypes { get; set; } } /// /// Represents an interaction response payload. /// internal sealed class RestInteractionResponsePayload : ObservableApiObject { /// /// Gets the type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public InteractionResponseType Type { get; set; } /// /// Gets the data. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionApplicationCommandCallbackData Data { get; set; } /// /// Gets the attachments. /// [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] public List Attachments { get; set; } // TODO: Implement if it gets added to the api /// /// Gets the callback hints. /// [JsonProperty("hints", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList? CallbackHints { get; set; } } /// /// Represents an interaction response modal payload. /// internal sealed class RestInteractionModalResponsePayload : ObservableApiObject { /// /// Gets the type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public InteractionResponseType Type { get; set; } /// /// Gets the data. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionApplicationCommandModalCallbackData Data { get; set; } // TODO: Implement if it gets added to the api /// /// Gets the callback hints. /// [JsonProperty("hints", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList? CallbackHints { get; set; } } /// /// Represents an interaction response iFrame payload. /// internal sealed class RestInteractionIframeResponsePayload : ObservableApiObject { /// /// Gets the type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public InteractionResponseType Type { get; set; } /// /// Gets the data. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionApplicationCommandIframeCallbackData Data { get; set; } // TODO: Implement if it gets added to the api /// /// Gets the callback hints. /// [JsonProperty("hints", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList? CallbackHints { get; set; } } /// /// Represents a followup message create payload. /// internal sealed class RestFollowupMessageCreatePayload : ObservableApiObject { /// /// Gets the content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; set; } /// /// Get whether the message is tts. /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool? IsTts { get; set; } /// /// Gets the embeds. /// [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Embeds { get; set; } /// /// Gets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] public DiscordMentions Mentions { get; set; } /// /// Gets the flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MessageFlags? Flags { get; set; } /// /// Gets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Components { get; set; } /// /// Gets attachments. /// [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] public List Attachments { get; set; } } /// /// Represents a role connection metadata payload. /// internal sealed class RestApplicationRoleConnectionMetadataPayload : ObservableApiObject { /// /// Gets the metadata type. /// [JsonProperty("type")] public ApplicationRoleConnectionMetadataType Type { get; set; } /// /// Gets the metadata key. /// [JsonProperty("key")] public string Key { get; set; } /// /// Gets the metadata name. /// [JsonProperty("name")] public string Name { get; set; } /// /// Gets the metadata description. /// [JsonProperty("description")] public string Description { get; set; } /// /// Gets the metadata name translations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] public Dictionary NameLocalizations { get; set; } /// /// Gets the metadata description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] public Dictionary DescriptionLocalizations { get; set; } } diff --git a/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs b/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs index 49b93b54a..e8907d36c 100644 --- a/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs +++ b/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs @@ -1,87 +1,87 @@ using System; using System.Collections.Generic; using DisCatSharp.Entities; using DisCatSharp.Enums; namespace DisCatSharp.Net.Models; /// /// Represents a application command edit model. /// public class ApplicationCommandEditModel : ObservableApiObject { /// /// Sets the command's new name. /// public Optional Name { internal get => this._name; set { if (value.Value.Length > 32) throw new ArgumentException("Application command name cannot exceed 32 characters.", nameof(value)); this._name = value; } } private Optional _name; /// /// Sets the command's new description /// public Optional Description { internal get => this._description; set { if (value.Value.Length > 100) throw new ArgumentException("Application command description cannot exceed 100 characters.", nameof(value)); this._description = value; } } private Optional _description; /// /// Sets the command's name localizations. /// public Optional NameLocalizations { internal get; set; } /// /// Sets the command's description localizations. /// public Optional DescriptionLocalizations { internal get; set; } /// /// Sets the command's new options. /// public Optional?> Options { internal get; set; } /// /// Sets the command's needed permissions. /// public Optional DefaultMemberPermissions { internal get; set; } /// /// Sets the command's allowed contexts. /// - public Optional?> AllowedContexts { internal get; set; } + public Optional?> AllowedContexts { internal get; set; } /// /// Sets the command's allowed integration types. /// public Optional?> IntegrationTypes { internal get; set; } /// /// Sets whether the command can be used in direct messages. /// public Optional DmPermission { internal get; set; } /// /// Sets whether the command is marked as NSFW. /// public Optional IsNsfw { internal get; set; } } diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index dc4120b27..6c91eb040 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -1,7254 +1,7254 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Entities.OAuth2; using DisCatSharp.Enums; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Abstractions.Rest; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net; /// /// Represents a discord api client. /// public sealed class DiscordApiClient { /// /// The audit log reason header name. /// private const string REASON_HEADER_NAME = CommonHeaders.AUDIT_LOG_REASON_HEADER; /// /// Gets the discord client. /// internal BaseDiscordClient Discord { get; } /// /// Gets the oauth2 client. /// internal DiscordOAuth2Client OAuth2Client { get; } /// /// Gets the rest client. /// internal RestClient Rest { get; } /// /// Initializes a new instance of the class. /// /// The base discord client. internal DiscordApiClient(BaseDiscordClient client) { this.Discord = client; this.OAuth2Client = null!; this.Rest = new(client); } /// /// Initializes a new instance of the class. /// /// The oauth2 client. /// The proxy. /// The timeout. /// If true, use relative rate limit. /// The logger. internal DiscordApiClient(DiscordOAuth2Client client, IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) { this.OAuth2Client = client; this.Discord = null!; this.Rest = new(proxy, timeout, useRelativeRateLimit, logger); } /// /// Initializes a new instance of the class. /// /// The proxy. /// The timeout. /// If true, use relative rate limit. /// The logger. internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) { this.Discord = null!; this.OAuth2Client = null!; this.Rest = new(proxy, timeout, useRelativeRateLimit, logger); } /// /// Builds the query string. /// /// The values. /// Whether this query will be transmitted via POST. private static string BuildQueryString(IDictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; var valsCollection = values.Select(xkvp => $"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}"); var vals = string.Join("&", valsCollection); return !post ? $"?{vals}" : vals; } /// /// Prepares the message. /// /// The raw message. private DiscordMessage PrepareMessage(JToken msgRaw) { var author = msgRaw["author"].ToObject(); var ret = msgRaw.ToDiscordObject(); ret.Discord = this.Discord; this.PopulateMessage(author, ret); var referencedMsg = msgRaw["referenced_message"]; if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) { author = referencedMsg["author"].ToObject(); ret.ReferencedMessage.Discord = this.Discord; this.PopulateMessage(author, ret.ReferencedMessage); } if (ret.Channel != null) return ret; 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; return ret; } /// /// Populates the message. /// /// The author. /// The message. private void PopulateMessage(TransportUser author, DiscordMessage ret) { var guild = ret.Channel?.Guild; //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 }; else { if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr)) this.Discord.UserCache[author.Id] = usr = new(author) { Discord = this.Discord }; if (guild != null) { if (!guild.Members.TryGetValue(author.Id, out var mbr)) mbr = new(usr) { Discord = this.Discord, GuildId = guild.Id }; ret.Author = mbr; } else ret.Author = usr; } ret.PopulateMentions(); ret.ReactionsInternal ??= []; foreach (var xr in ret.ReactionsInternal) xr.Emoji.Discord = this.Discord; } /// /// Executes a rest request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The payload. /// The ratelimit wait override. /// Enables a possible breakpoint in the rest client for debugging purposes. internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary? headers = null, string? payload = null, double? ratelimitWaitOverride = null, bool targetDebug = false) { var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride); if (this.Discord is not null) this.Rest.ExecuteRequestAsync(req, targetDebug).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, $"Error while executing request. Url: {url.AbsoluteUri}"); else _ = this.Rest.ExecuteRequestAsync(req, targetDebug); return req.WaitForCompletionAsync(); } /// /// Executes a rest form data request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The form data. /// The ratelimit wait override. /// Enables a possible breakpoint in the rest client for debugging purposes. internal Task DoFormRequestAsync(DiscordOAuth2Client client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, Dictionary formData, Dictionary? headers = null, double? ratelimitWaitOverride = null, bool targetDebug = false) { var req = new RestFormRequest(client, bucket, url, method, route, formData, headers, ratelimitWaitOverride); this.Rest.ExecuteFormRequestAsync(req, targetDebug).LogTaskFault(this.OAuth2Client.Logger, LogLevel.Error, LoggerEvents.RestError, $"Error while executing request. Url: {url.AbsoluteUri}"); return req.WaitForCompletionAsync(); } /// /// Executes a multipart rest request for stickers. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The sticker name. /// The sticker tag. /// The sticker description. /// The headers. /// The file. /// The ratelimit wait override. /// Enables a possible breakpoint in the rest client for debugging purposes. private Task DoStickerMultipartAsync( BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, string name, string tags, string? description = null, IReadOnlyDictionary? headers = null, DiscordMessageFile? file = null, double? ratelimitWaitOverride = null, bool targetDebug = false ) { var req = new MultipartStickerWebRequest(client, bucket, url, method, route, name, tags, description, headers, file, ratelimitWaitOverride); if (this.Discord is not null) this.Rest.ExecuteRequestAsync(req, targetDebug).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req, targetDebug); return req.WaitForCompletionAsync(); } /// /// Executes a multipart request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The values. /// The files. /// The ratelimit wait override. /// Enables a possible breakpoint in the rest client for debugging purposes. private Task DoMultipartAsync( BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary? headers = null, IReadOnlyDictionary? values = null, IEnumerable? files = null, double? ratelimitWaitOverride = null, bool targetDebug = false ) { var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride); if (this.Discord is not null) this.Rest.ExecuteRequestAsync(req, targetDebug).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req, targetDebug); return req.WaitForCompletionAsync(); } // begin todo #region Guild /// /// Gets the guild async. /// /// The guild id. /// If true, with_counts. internal async Task GetGuildAsync(ulong guildId, bool? withCounts) { var urlParams = new Dictionary(); if (withCounts.HasValue) urlParams["with_counts"] = withCounts?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, urlParams).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guildRest = DiscordJson.DeserializeObject(res.Response, this.Discord); foreach (var r in guildRest.RolesInternal.Values) r.GuildId = guildRest.Id; if (this.Discord is DiscordClient dc) { await dc.OnGuildUpdateEventAsync(guildRest, rawMembers).ConfigureAwait(false); return dc.GuildsInternal[guildRest.Id]; } else { guildRest.Discord = this.Discord; return guildRest; } } /// /// Searches the members async. /// /// The guild_id. /// The name. /// The limit. internal async Task> SearchMembersAsync(ulong guildId, string name, int? limit) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var querydict = new Dictionary { ["query"] = name, ["limit"] = limit.ToString() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var tms = json.ToObject>(); var mbrs = new List(); foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old.BannerColorInternal = usr.BannerColorInternal; old.AvatarDecorationData = usr.AvatarDecorationData; old.ThemeColorsInternal = usr.ThemeColorsInternal; old.Pronouns = usr.Pronouns; old.Locale = usr.Locale; old.GlobalName = usr.GlobalName; return old; }); mbrs.Add(new(xtm) { Discord = this.Discord, GuildId = guildId }); } return mbrs; } /// /// Gets the guild ban async. /// /// The guild_id. /// The user_id. internal async Task GetGuildBanAsync(ulong guildId, ulong userId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId, user_id = userId }, out var path); var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ban = json.ToObject(); return ban; } /// /// Creates the guild async. /// /// The name. /// The region_id. /// The iconb64. /// The verification_level. /// The default_message_notifications. /// The system_channel_flags. internal async Task CreateGuildAsync( string name, string regionId, Optional iconb64, VerificationLevel? verificationLevel, DefaultMessageNotifications? defaultMessageNotifications, SystemChannelFlags? systemChannelFlags ) { var pld = new RestGuildCreatePayload { Name = name, RegionId = regionId, DefaultMessageNotifications = defaultMessageNotifications, VerificationLevel = verificationLevel, IconBase64 = iconb64, SystemChannelFlags = systemChannelFlags }; var route = $"{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false); return guild; } /// /// Creates the guild from template async. /// /// The template_code. /// The name. /// The iconb64. internal async Task CreateGuildFromTemplateAsync(string templateCode, string name, Optional iconb64) { var pld = new RestGuildCreateFromTemplatePayload { Name = name, IconBase64 = iconb64 }; var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { template_code = templateCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false); return guild; } /// /// Deletes the guild async. /// /// The guild_id. internal async Task DeleteGuildAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); if (this.Discord is DiscordClient dc) { var gld = dc.GuildsInternal[guildId]; await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false); } } /// /// Modifies the guild. /// /// The guild id. /// The name. /// The verification level. /// The default message notifications. /// The mfa level. /// The explicit content filter. /// The afk channel id. /// The afk timeout. /// The iconb64. /// The owner id. /// The splashb64. /// The system channel id. /// The system channel flags. /// The public updates channel id. /// The rules channel id. /// The description. /// The banner base64. /// The discovery base64. /// The home header base64. /// The preferred locale. /// Whether the premium progress bar should be enabled. /// The reason. internal async Task ModifyGuildAsync( ulong guildId, Optional name, Optional verificationLevel, Optional defaultMessageNotifications, Optional mfaLevel, Optional explicitContentFilter, Optional afkChannelId, Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64, Optional systemChannelId, Optional systemChannelFlags, Optional publicUpdatesChannelId, Optional rulesChannelId, Optional description, Optional bannerb64, Optional discoverySplashb64, Optional homeHeaderb64, Optional preferredLocale, Optional premiumProgressBarEnabled, string? reason ) { var pld = new RestGuildModifyPayload { Name = name, VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, MfaLevel = mfaLevel, ExplicitContentFilter = explicitContentFilter, AfkChannelId = afkChannelId, AfkTimeout = afkTimeout, IconBase64 = iconb64, SplashBase64 = splashb64, BannerBase64 = bannerb64, DiscoverySplashBase64 = discoverySplashb64, OwnerId = ownerId, SystemChannelId = systemChannelId, SystemChannelFlags = systemChannelFlags, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description, PremiumProgressBarEnabled = premiumProgressBarEnabled, HomeHeaderBase64 = homeHeaderb64 }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Modifies the guild community settings. /// /// The guild id. /// The guild features. /// The rules channel id. /// The public updates channel id. /// The preferred locale. /// The description. /// The default message notifications. /// The explicit content filter. /// The verification level. /// The reason. internal async Task ModifyGuildCommunitySettingsAsync(ulong guildId, List features, Optional rulesChannelId, Optional publicUpdatesChannelId, string preferredLocale, string description, DefaultMessageNotifications defaultMessageNotifications, ExplicitContentFilter explicitContentFilter, VerificationLevel verificationLevel, string? reason) { var pld = new RestGuildCommunityModifyPayload { VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, ExplicitContentFilter = explicitContentFilter, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = Optional.FromNullable(description), Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Modifies the guilds inventory settings. /// /// The guild id. /// Whether emoji packs are collectible. /// The reason. internal async Task ModifyGuildInventorySettingsAsync(ulong guildId, bool isEmojiPackCollectible, string? reason) { var pld = new RestGuildInventoryModifyPayload { IsEmojiPackCollectible = isEmojiPackCollectible }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INVENTORY}{Endpoints.SETTINGS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds[guildId].InventorySettings = new() { IsEmojiPackCollectible = isEmojiPackCollectible }; return this.Discord.Guilds[guildId]; } /// /// Modifies the guilds incident actions. /// /// The guild id. /// Until when invites are disabled. Set to disable. /// Until when direct messages are disabled. Set to disable. internal async Task ModifyGuildIncidentActionsAsync(ulong guildId, DateTimeOffset? invitesDisabledUntil, DateTimeOffset? dmsDisabledUntil) { var pld = new RestGuildIncidentActionsModifyPayload { InvitesDisabledUntil = invitesDisabledUntil, DmsDisabledUntil = dmsDisabledUntil }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INCIDENT_ACTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var data = DiscordJson.DeserializeObject(res.Response, this.Discord); this.Discord.Guilds[guildId].IncidentsData = data; return data; } /// /// Gets the guilds onboarding. /// /// The guild id. internal async Task GetGuildOnboardingAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ONBOARDING}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var onboarding = DiscordJson.DeserializeObject(res.Response, this.Discord); return onboarding; } /// /// Modifies the guilds onboarding. /// /// The guild id. /// The onboarding prompts /// The default channel ids. /// Whether onboarding is enabled. /// The onboarding mode. /// The reason. internal async Task ModifyGuildOnboardingAsync(ulong guildId, Optional> prompts, Optional> defaultChannelIds, Optional enabled, Optional mode, string? reason = null) { var pld = new RestGuildOnboardingModifyPayload() { Prompts = prompts, DefaultChannelIds = defaultChannelIds, Enabled = enabled, Mode = mode }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ONBOARDING}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var onboarding = DiscordJson.DeserializeObject(res.Response, this.Discord); return onboarding; } /// /// Gets the guilds server guide. /// /// The guild id. internal async Task GetGuildServerGuideAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.NEW_MEMBER_WELCOME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var guide = DiscordJson.DeserializeObject(res.Response, this.Discord); return guide; } /// /// Modifies the guilds server guide. /// /// The guild id. /// Whether the server guide is enabled. /// The server guide welcome message. /// The new member actions. /// The resource channels. /// The reason. internal async Task ModifyGuildServerGuideAsync(ulong guildId, Optional enabled, Optional welcomeMessage, Optional> newMemberActions, Optional> resourceChannels, string? reason = null) { var pld = new RestGuildServerGuideModifyPayload() { Enabled = enabled, WelcomeMessage = welcomeMessage, NewMemberActions = newMemberActions, ResourceChannels = resourceChannels }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.NEW_MEMBER_WELCOME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var guide = DiscordJson.DeserializeObject(res.Response, this.Discord); return guide; } /// /// Modifies the guild safety settings. /// /// The guild id. /// The guild features. /// The safety alerts channel id. /// The reason. internal async Task ModifyGuildSafetyAlertsSettingsAsync(ulong guildId, List features, Optional safetyAlertsChannelId, string? reason) { var pld = new RestGuildSafetyModifyPayload { SafetyAlertsChannelId = safetyAlertsChannelId, Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Modifies the guild features. /// /// The guild id. /// The guild features. /// The reason. /// internal async Task ModifyGuildFeaturesAsync(ulong guildId, List features, string? reason) { var pld = new RestGuildFeatureModifyPayload { Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Enables the guilds mfa requirement. /// /// The guild id. /// The reason. internal async Task EnableGuildMfaAsync(ulong guildId, string? reason) { var pld = new RestGuildMfaLevelModifyPayload { Level = MfaLevel.Enabled }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MFA}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Disables the guilds mfa requirement. /// /// The guild id. /// The reason. internal async Task DisableGuildMfaAsync(ulong guildId, string? reason) { var pld = new RestGuildMfaLevelModifyPayload { Level = MfaLevel.Disabled }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MFA}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Implements https://discord.com/developers/docs/resources/guild#get-guild-bans. /// internal async Task> GetGuildBansAsync(ulong guildId, 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 = guildId }, out var path); var urlParams = new Dictionary(); if (limit != null) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlParams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var bansRaw = JsonConvert.DeserializeObject>(res.Response).Select(xb => { if (!this.Discord.TryGetCachedUserInternal(xb.RawUser.Id, out var usr)) { usr = new(xb.RawUser) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.GlobalName = usr.GlobalName; return old; }); } xb.User = usr; return xb; }); var bans = new ReadOnlyCollection(new List(bansRaw)); return bans; } /// /// Creates the guild ban async. /// /// The guild_id. /// The user_id. /// The delete_message_days. /// The reason. internal Task CreateGuildBanAsync(ulong guildId, ulong userId, int deleteMessageDays, string? reason) { if (deleteMessageDays < 0 || deleteMessageDays > 7) throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(deleteMessageDays)); var urlParams = new Dictionary { ["delete_message_days"] = deleteMessageDays.ToString(CultureInfo.InvariantCulture) }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild ban async. /// /// The guild_id. /// The user_id. /// The reason. internal Task RemoveGuildBanAsync(ulong guildId, ulong userId, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(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 = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Leaves the guild async. /// /// The guild_id. internal Task LeaveGuildAsync(ulong guildId) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the guild member async. /// /// The guild id. /// The user id. /// The user's access token. /// The nickname. /// The roles. /// If true, muted. /// If true, deafened. internal async Task AddGuildMemberAsync(ulong guildId, ulong userId, string accessToken, string? nickname = null, IEnumerable? roles = null, bool? muted = null, bool? deafened = null) { var pld = new RestGuildMemberAddPayload { AccessToken = accessToken, Nickname = nickname, Roles = roles, Deaf = deafened, Mute = muted }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var tm = DiscordJson.DeserializeObject(res.Response, this.Discord); tm.Discord = this.Discord; tm.GuildId = guildId; return tm; } /// /// Lists the guild members async. /// /// The guild_id. /// The limit. /// The after. internal async Task> ListGuildMembersAsync(ulong guildId, int? limit, ulong? after) { var urlParams = new Dictionary(); if (limit is > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlParams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var membersRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(membersRaw); } /// /// Adds the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. internal Task AddGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); 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 = guildId, user_id = userId, role_id = roleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. internal Task RemoveGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); 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 = guildId, user_id = userId, role_id = roleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Modifies the guild channel position async. /// /// The guild_id. /// The pld. /// The reason. internal Task ModifyGuildChannelPositionAsync(ulong guildId, IEnumerable pld, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. internal Task ModifyGuildChannelParentAsync(ulong guildId, IEnumerable pld, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Detaches the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. internal Task DetachGuildChannelParentAsync(ulong guildId, IEnumerable pld, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild role position async. /// /// The guild_id. /// The pld. /// The reason. internal Task ModifyGuildRolePositionAsync(ulong guildId, IEnumerable pld, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the audit logs async. /// /// The guild_id. /// The limit. /// The after. /// The before. /// The responsible. /// The action_type. internal async Task GetAuditLogsAsync(ulong guildId, int limit, ulong? after, ulong? before, ulong? responsible, int? actionType) { 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 (actionType != null) urlParams["action_type"] = actionType?.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUDIT_LOGS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var auditLogDataRaw = DiscordJson.DeserializeObject(res.Response, this.Discord); return auditLogDataRaw; } /// /// Gets the guild vanity url async. /// /// The guild_id. internal async Task GetGuildVanityUrlAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VANITY_URL}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invite = DiscordJson.DeserializeObject(res.Response, this.Discord); return invite; } /// /// Gets the guild widget async. /// /// The guild_id. internal async Task GetGuildWidgetAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET_JSON}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawChannels = (JArray)json["channels"]; var ret = json.ToDiscordObject(); ret.Discord = this.Discord; ret.Guild = this.Discord.Guilds.ContainsKey(guildId) ? this.Discord.Guilds[guildId] : null; 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(); return ret; } /// /// Gets the guild widget settings async. /// /// The guild_id. internal async Task GetGuildWidgetSettingsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Guild = this.Discord.Guilds[guildId]; return ret; } /// /// Modifies the guild widget settings async. /// /// The guild_id. /// If true, is enabled. /// The channel id. /// The reason. internal async Task ModifyGuildWidgetSettingsAsync(ulong guildId, bool? isEnabled, ulong? channelId, string? reason) { var pld = new RestGuildWidgetSettingsPayload { Enabled = isEnabled, ChannelId = channelId }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Guild = this.Discord.Guilds[guildId]; return ret; } /// /// Gets the guild templates async. /// /// The guild_id. internal async Task> GetGuildTemplatesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templatesRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(templatesRaw)); } /// /// Creates the guild template async. /// /// The guild_id. /// The name. /// The description. internal async Task CreateGuildTemplateAsync(ulong guildId, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Syncs the guild template async. /// /// The guild_id. /// The template_code. internal async Task SyncGuildTemplateAsync(ulong guildId, string templateCode) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id = guildId, template_code = templateCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route).ConfigureAwait(false); var templateRaw = DiscordJson.DeserializeObject(res.Response, this.Discord); return templateRaw; } /// /// Modifies the guild template async. /// /// The guild_id. /// The template_code. /// The name. /// The description. internal async Task ModifyGuildTemplateAsync(ulong guildId, string templateCode, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, template_code = templateCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var templateRaw = DiscordJson.DeserializeObject(res.Response, this.Discord); return templateRaw; } /// /// Deletes the guild template async. /// /// The guild_id. /// The template_code. internal async Task DeleteGuildTemplateAsync(ulong guildId, string templateCode) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id = guildId, template_code = templateCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); var templateRaw = DiscordJson.DeserializeObject(res.Response, this.Discord); return templateRaw; } /// /// Gets the guild membership screening form async. /// /// The guild_id. internal async Task GetGuildMembershipScreeningFormAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var screeningRaw = DiscordJson.DeserializeObject(res.Response, this.Discord); return screeningRaw; } /// /// Modifies the guild membership screening form async. /// /// The guild_id. /// The enabled. /// The fields. /// The description. internal async Task ModifyGuildMembershipScreeningFormAsync(ulong guildId, Optional enabled, Optional fields, Optional description) { var pld = new RestGuildMembershipScreeningFormModifyPayload { Enabled = enabled, Description = description, Fields = fields }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var screeningRaw = DiscordJson.DeserializeObject(res.Response, this.Discord); return screeningRaw; } /// /// Gets the guild welcome screen async. /// /// The guild_id. internal async Task GetGuildWelcomeScreenAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Modifies the guild welcome screen async. /// /// The guild_id. /// The enabled. /// The welcome channels. /// The description. internal async Task ModifyGuildWelcomeScreenAsync(ulong guildId, Optional enabled, Optional> welcomeChannels, Optional description) { var pld = new RestGuildWelcomeScreenModifyPayload { Enabled = enabled, WelcomeChannels = welcomeChannels, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Updates the current user voice state async. /// /// The guild_id. /// The channel id. /// If true, suppress. /// The request to speak timestamp. internal async Task UpdateCurrentUserVoiceStateAsync(ulong guildId, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp) { var pld = new RestGuildUpdateCurrentUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress, RequestToSpeakTimestamp = requestToSpeakTimestamp }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Updates the user voice state async. /// /// The guild_id. /// The user_id. /// The channel id. /// If true, suppress. internal async Task UpdateUserVoiceStateAsync(ulong guildId, ulong userId, ulong channelId, bool? suppress) { var pld = new RestGuildUpdateUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Gets all auto mod rules for a guild. /// /// The guild id. /// A collection of all auto mod rules in the guild. internal async Task> GetAutomodRulesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id/auto-moderation/rules"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var r in ret) r.Discord = this.Discord; return ret.AsReadOnly(); } /// /// Gets a specific auto mod rule in the guild. /// /// The guild id for the rule. /// The rule id. /// The rule if one is found. internal async Task GetAutomodRuleAsync(ulong guildId, ulong ruleId) { var route = $"{Endpoints.GUILDS}/:guild_id/auto-moderation/rules/:rule_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId, rule_id = ruleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } /// /// Creates an auto mod rule. /// /// The guild id of the rule. /// The name of the rule. /// The event type of the rule. /// The trigger type. /// The actions of the rule. /// The metadata of the rule. /// Whether this rule is enabled. /// The exempt roles of the rule. /// The exempt channels of the rule. /// The reason for this addition. /// The new auto mod rule. internal async Task CreateAutomodRuleAsync( ulong guildId, string name, AutomodEventType eventType, AutomodTriggerType triggerType, IEnumerable actions, AutomodTriggerMetadata triggerMetadata = null, bool enabled = false, IEnumerable exemptRoles = null, IEnumerable exemptChannels = null, string? reason = null ) { var route = $"{Endpoints.GUILDS}/:guild_id/auto-moderation/rules"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); RestAutomodRuleModifyPayload pld = new() { Name = name, EventType = eventType, TriggerType = triggerType, Actions = actions.ToArray(), Enabled = enabled, TriggerMetadata = triggerMetadata ?? null }; if (exemptChannels != null) pld.ExemptChannels = exemptChannels.ToArray(); if (exemptRoles != null) pld.ExemptRoles = exemptRoles.ToArray(); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; if (this.Discord is DiscordClient dc) await dc.OnAutomodRuleCreated(ret).ConfigureAwait(false); return ret; } /// /// Modifies an auto mod role /// /// The guild id. /// The rule id. /// The new name of the rule. /// The new event type of the rule. /// The new metadata of the rule. /// The new actions of the rule. /// Whether this rule is enabled. /// The new exempt roles of the rule. /// The new exempt channels of the rule. /// The reason for this modification. /// The updated automod rule internal async Task ModifyAutomodRuleAsync( ulong guildId, ulong ruleId, Optional name, Optional eventType, Optional metadata, Optional> actions, Optional enabled, Optional> exemptRoles, Optional> exemptChannels, string? reason = null ) { var pld = new RestAutomodRuleModifyPayload { Name = name, EventType = eventType, TriggerMetadata = metadata, Enabled = enabled }; if (actions.HasValue) pld.Actions = actions.Value?.ToArray(); if (exemptChannels.HasValue) pld.ExemptChannels = exemptChannels.Value?.ToArray(); if (exemptRoles.HasValue) pld.ExemptRoles = exemptRoles.Value?.ToArray(); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id/auto-moderation/rules/:rule_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, rule_id = ruleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; if (this.Discord is DiscordClient dc) await dc.OnAutomodRuleUpdated(ret).ConfigureAwait(false); return ret; } /// /// Deletes an auto mod rule. /// /// The guild id of the rule. /// The rule id. /// The reason for this deletion. /// The deleted auto mod rule. internal async Task DeleteAutomodRuleAsync(ulong guildId, ulong ruleId, string? reason = null) { var route = $"{Endpoints.GUILDS}/:guild_id/auto-moderation/rules/:rule_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id = guildId, rule_id = ruleId }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; if (this.Discord is DiscordClient dc) await dc.OnAutomodRuleDeleted(ret).ConfigureAwait(false); return ret; } #endregion // End todo #region Guild Scheduled Events /// /// Creates a scheduled event. /// internal async Task CreateGuildScheduledEventAsync(ulong guildId, ulong? channelId, DiscordScheduledEventEntityMetadata metadata, string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime, string description, ScheduledEventEntityType type, Optional coverb64, string? reason = null) { var pld = new RestGuildScheduledEventCreatePayload { ChannelId = channelId, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduledStartTime, ScheduledEndTime = scheduledEndTime, Description = description, EntityType = type, CoverBase64 = coverb64 }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var scheduledEvent = DiscordJson.DeserializeObject(res.Response, this.Discord); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) scheduledEvent.Creator.Discord = this.Discord; if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventCreateEventAsync(scheduledEvent, guild).ConfigureAwait(false); return scheduledEvent; } /// /// Modifies a scheduled event. /// internal async Task ModifyGuildScheduledEventAsync( ulong guildId, ulong scheduledEventId, Optional channelId, Optional metadata, Optional name, Optional scheduledStartTime, Optional scheduledEndTime, Optional description, Optional type, Optional status, Optional coverb64, string? reason = null ) { var pld = new RestGuildScheduledEventModifyPayload { ChannelId = channelId, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduledStartTime, ScheduledEndTime = scheduledEndTime, Description = description, EntityType = type, Status = status, CoverBase64 = coverb64 }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var scheduledEvent = DiscordJson.DeserializeObject(res.Response, this.Discord); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; old.GlobalName = scheduledEvent.Creator.GlobalName; return old; }); } if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventUpdateEventAsync(scheduledEvent, guild).ConfigureAwait(false); return scheduledEvent; } /// /// Modifies a scheduled event. /// internal async Task ModifyGuildScheduledEventStatusAsync(ulong guildId, ulong scheduledEventId, ScheduledEventStatus status, string? reason = null) { var pld = new RestGuildScheduledEventModifyPayload { Status = status }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var scheduledEvent = DiscordJson.DeserializeObject(res.Response, this.Discord); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; old.GlobalName = scheduledEvent.Creator.GlobalName; return old; }); } if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventUpdateEventAsync(scheduledEvent, guild).ConfigureAwait(false); return scheduledEvent; } /// /// Gets a scheduled event. /// /// The guild_id. /// The event id. /// Whether to include user count. internal async Task GetGuildScheduledEventAsync(ulong guildId, ulong scheduledEventId, bool? withUserCount) { var urlParams = new Dictionary(); if (withUserCount.HasValue) urlParams["with_user_count"] = withUserCount?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var scheduledEvent = DiscordJson.DeserializeObject(res.Response, this.Discord); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; old.GlobalName = scheduledEvent.Creator.GlobalName; return old; }); } return scheduledEvent; } /// /// Gets the guilds scheduled events. /// /// The guild_id. /// Whether to include the count of users subscribed to the scheduled event. internal async Task> ListGuildScheduledEventsAsync(ulong guildId, bool? withUserCount) { var urlParams = new Dictionary(); if (withUserCount.HasValue) urlParams["with_user_count"] = withUserCount?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var events = new Dictionary(); var eventsRaw = JsonConvert.DeserializeObject>(res.Response); var guild = this.Discord.Guilds[guildId]; foreach (var ev in eventsRaw) { ev.Discord = this.Discord; if (ev.Creator != null) { ev.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(ev.Creator.Id, ev.Creator, (id, old) => { old.Username = ev.Creator.Username; old.Discriminator = ev.Creator.Discriminator; old.AvatarHash = ev.Creator.AvatarHash; old.Flags = ev.Creator.Flags; return old; }); } events.Add(ev.Id, ev); } return new ReadOnlyDictionary(new Dictionary(events)); } /// /// Deletes a guild scheduled event. /// /// The guild_id. /// The scheduled event id. /// The reason. internal Task DeleteGuildScheduledEventAsync(ulong guildId, ulong scheduledEventId, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the users who RSVP'd to a scheduled event. /// Optional with member objects. /// This endpoint is paginated. /// /// The guild_id. /// The scheduled event id. /// The limit how many users to receive from the event. /// Get results before the given id. /// Get results after the given id. /// Whether to include guild member data. attaches guild_member property to the user object. internal async Task> GetGuildScheduledEventRspvUsersAsync(ulong guildId, ulong scheduledEventId, int? limit, ulong? before, ulong? after, bool? withMember) { var urlParams = new Dictionary(); if (limit is > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlParams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); if (withMember != null) urlParams["with_member"] = withMember.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id{Endpoints.USERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var rspvUsers = JsonConvert.DeserializeObject>(res.Response); Dictionary rspv = []; foreach (var rspvUser in rspvUsers) { rspvUser.Discord = this.Discord; rspvUser.GuildId = guildId; rspvUser.User.Discord = this.Discord; rspvUser.User = this.Discord.UserCache.AddOrUpdate(rspvUser.User.Id, rspvUser.User, (id, old) => { old.Username = rspvUser.User.Username; old.Discriminator = rspvUser.User.Discriminator; old.AvatarHash = rspvUser.User.AvatarHash; old.BannerHash = rspvUser.User.BannerHash; old.BannerColorInternal = rspvUser.User.BannerColorInternal; return old; }); /*if (with_member.HasValue && with_member.Value && rspv_user.Member != null) { rspv_user.Member.Discord = this.Discord; }*/ rspv.Add(rspvUser.User.Id, rspvUser); } return new ReadOnlyDictionary(new Dictionary(rspv)); } #endregion // begin todo #region Channel /// /// Creates a guild channel. /// /// The guild_id. /// The name. /// The type. /// The parent. /// The topic. /// The bitrate. /// The user_limit. /// The overwrites. /// If true, nsfw. /// The per user rate limit. /// The quality mode. /// The default auto archive duration. /// The channel flags. /// The reason. internal async Task CreateGuildChannelAsync(ulong guildId, string name, ChannelType type, ulong? parent, Optional topic, int? bitrate, int? userLimit, IEnumerable overwrites, bool? nsfw, Optional perUserRateLimit, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? defaultAutoArchiveDuration, Optional flags, string? reason) { var restOverwrites = new List(); if (overwrites != null) foreach (var ow in overwrites) restOverwrites.Add(ow.Build()); var pld = new RestChannelCreatePayload { Name = name, Type = type, Parent = parent, Topic = topic, Bitrate = bitrate, UserLimit = userLimit, PermissionOverwrites = restOverwrites, Nsfw = nsfw, PerUserRateLimit = perUserRateLimit, QualityMode = qualityMode, DefaultAutoArchiveDuration = defaultAutoArchiveDuration, Flags = flags }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Initialize(this.Discord); return ret; } /// /// Creates a guild forum channel. /// /// The guild_id. /// The name. /// The parent. /// The topic. /// The template. /// The default reaction emoji. /// The overwrites. /// If true, nsfw. /// The per user rate limit. /// The per user post create rate limit. /// The default auto archive duration. /// The default sort order. /// The channel flags. /// The reason. internal async Task CreateForumChannelAsync( ulong guildId, string name, ulong? parent, Optional topic, Optional template, bool? nsfw, Optional defaultReactionEmoji, Optional perUserRateLimit, Optional postCreateUserRateLimit, Optional defaultSortOrder, ThreadAutoArchiveDuration? defaultAutoArchiveDuration, IEnumerable permissionOverwrites, Optional flags, string? reason ) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = []; foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelCreatePayload { Name = name, Topic = topic, //Template = template, Nsfw = nsfw, Parent = parent, PerUserRateLimit = perUserRateLimit, PostCreateUserRateLimit = postCreateUserRateLimit, DefaultAutoArchiveDuration = defaultAutoArchiveDuration, DefaultReactionEmoji = defaultReactionEmoji, PermissionOverwrites = restoverwrites, DefaultSortOrder = defaultSortOrder, Flags = flags, Type = ChannelType.Forum }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Initialize(this.Discord); return ret; } /// /// Modifies the channel async. /// /// The channel_id. /// The name. /// The position. /// The topic. /// If true, nsfw. /// The parent. /// The bitrate. /// The user_limit. /// The per user rate limit. /// The rtc region. /// The quality mode. /// The default auto archive duration. /// The type. /// The permission overwrites. /// The channel flags. /// The reason. internal Task ModifyChannelAsync( ulong channelId, string name, int? position, Optional topic, bool? nsfw, Optional parent, Optional bitrate, Optional userLimit, Optional perUserRateLimit, Optional rtcRegion, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? autoArchiveDuration, Optional type, IEnumerable permissionOverwrites, Optional flags, string? reason ) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = []; foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelModifyPayload { Name = name, Position = position, Topic = topic, Nsfw = nsfw, Parent = parent, Bitrate = bitrate, UserLimit = userLimit, PerUserRateLimit = perUserRateLimit, RtcRegion = rtcRegion, QualityMode = qualityMode, DefaultAutoArchiveDuration = autoArchiveDuration, Type = type, Flags = flags, PermissionOverwrites = restoverwrites }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the forum channel. /// /// The channel id. /// The name. /// The parent. /// The position. /// The topic. /// The template. /// The default reaction emoji. /// The overwrites. /// If true, nsfw. /// The available tags. /// The default sort order. /// The per user rate limit. /// The per user post create rate limit. /// The default auto archive duration. /// The channel flags. /// The reason. internal async Task ModifyForumChannelAsync( ulong channelId, string name, int? position, Optional topic, Optional template, bool? nsfw, Optional parent, Optional?> availableTags, Optional defaultReactionEmoji, Optional perUserRateLimit, Optional postCreateUserRateLimit, Optional defaultSortOrder, Optional defaultAutoArchiveDuration, IEnumerable permissionOverwrites, Optional flags, string? reason ) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = []; foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelModifyPayload { Name = name, Position = position, Topic = topic, //Template = template, Nsfw = nsfw, Parent = parent, PerUserRateLimit = perUserRateLimit, PostCreateUserRateLimit = postCreateUserRateLimit, DefaultAutoArchiveDuration = defaultAutoArchiveDuration, DefaultReactionEmoji = defaultReactionEmoji, PermissionOverwrites = restoverwrites, DefaultSortOrder = defaultSortOrder, Flags = flags, AvailableTags = availableTags }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Initialize(this.Discord); return ret; } /// /// Gets the channel async. /// /// The channel_id. internal async Task GetChannelAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Initialize(this.Discord); return ret; } /// /// Deletes the channel async. /// /// The channel_id. /// The reason. internal Task DeleteChannelAsync(ulong channelId, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the message async. /// /// The channel_id. /// The message_id. internal async Task GetMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The content. /// The embeds. /// The sticker. /// The reply message id. /// If true, mention reply. /// If true, fail on invalid reply. /// The components. internal async Task CreateMessageAsync(ulong channelId, string content, IEnumerable embeds, DiscordSticker sticker, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply, ReadOnlyCollection? components = null) { if (content is { Length: > 2000 }) throw new ArgumentException("Message content length cannot exceed 2000 characters."); if (!embeds?.Any() ?? true) { if (content == null && sticker == null && components == null) throw new ArgumentException("You must specify message content, a sticker, components or an embed."); if (content.Length == 0) throw new ArgumentException("Message content must not be empty."); } 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, StickersIds = sticker is null ? Array.Empty() : [sticker.Id], IsTts = false, HasEmbed = embeds?.Any() ?? false, Embeds = embeds, Components = components }; if (replyMessageId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = replyMessageId, FailIfNotExists = failOnInvalidReply }; if (replyMessageId != null) pld.Mentions = new(Mentions.All, true, mentionReply); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The builder. internal async Task CreateMessageAsync(ulong channelId, DiscordMessageBuilder builder) { builder.Validate(); 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.Sticker is null ? Array.Empty() : [builder.Sticker.Id], IsTts = builder.IsTts, HasEmbed = builder.Embeds != null, Embeds = builder.Embeds, Components = builder.Components, Nonce = builder.Nonce, EnforceNonce = builder.EnforceNonce }; if (builder.ReplyId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; pld.Mentions = new(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false, builder.MentionOnReply); if (builder.Files.Count == 0) { if (builder.Attachments.Any()) { ulong fileId = 0; List attachments = new(builder.Attachments.Count); foreach (var att in builder.Attachments) { att.Id = fileId; attachments.Add(att); fileId++; } pld.Attachments = attachments; } var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } else { ulong fileId = 0; List attachments = new(builder.Files.Count); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, Filename = file.Filename }; attachments.Add(att); fileId++; } pld.Attachments = attachments; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in builder.FilesInternal.Where(x => x.ResetPositionTo.HasValue)) file.Stream.Position = file.ResetPositionTo.Value; return ret; } } /// /// Gets the guild channels async. /// /// The guild_id. internal async Task> GetGuildChannelsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var channelsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); foreach (var ret in channelsRaw) ret.Initialize(this.Discord); return new ReadOnlyCollection(new List(channelsRaw)); } /// /// Modifies a voice channels status. /// /// The voice channel id. /// The status. internal Task ModifyVoiceChannelStatusAsync(ulong channelId, string? status) { var pld = new RestVoiceChannelStatusModifyPayload { Status = status }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.VOICE_STATUS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Creates the stage instance async. /// /// The channel_id. /// The topic. /// Whether everyone should be notified about the stage. /// The associated scheduled event id. /// The reason. internal async Task CreateStageInstanceAsync(ulong channelId, string topic, bool sendStartNotification, ulong? scheduledEventId = null, string? reason = null) { var pld = new RestStageInstanceCreatePayload { ChannelId = channelId, Topic = topic, ScheduledEventId = scheduledEventId, SendStartNotification = sendStartNotification }; var route = $"{Endpoints.STAGE_INSTANCES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var stageInstance = DiscordJson.DeserializeObject(res.Response, this.Discord); return stageInstance; } /// /// Gets the stage instance async. /// /// The channel_id. internal async Task GetStageInstanceAsync(ulong channelId) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var stageInstance = DiscordJson.DeserializeObject(res.Response, this.Discord); return stageInstance; } /// /// Modifies the stage instance async. /// /// The channel_id. /// The topic. /// The reason. internal Task ModifyStageInstanceAsync(ulong channelId, Optional topic, string? reason) { var pld = new RestStageInstanceModifyPayload { Topic = topic }; var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id = channelId }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Deletes the stage instance async. /// /// The channel_id. /// The reason. internal Task DeleteStageInstanceAsync(ulong channelId, string? reason) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the channel messages async. /// /// The channel id. /// The limit. /// The before. /// The after. /// The around. internal async Task> GetChannelMessagesAsync(ulong channelId, int limit, ulong? before, ulong? after, ulong? around) { 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 = channelId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgsRaw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgsRaw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Gets the channel message async. /// /// The channel_id. /// The message_id. internal async Task GetChannelMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Edits the message async. /// /// The channel_id. /// The message_id. /// The content. /// The embeds. /// The mentions. /// The components. /// The suppress_embed. /// The files. /// The attachments to keep. internal async Task EditMessageAsync(ulong channelId, ulong messageId, Optional content, Optional> embeds, Optional> mentions, IReadOnlyList components, Optional suppressEmbed, IReadOnlyCollection files, Optional> attachments) { if (embeds is { HasValue: true, Value: not 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.ValueOrDefault(), HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, Components = components, Flags = suppressEmbed.HasValue && (bool)suppressEmbed ? MessageFlags.SuppressedEmbeds : null, Mentions = mentions .Map(m => new DiscordMentions(m ?? Mentions.None, false, mentions.Value?.OfType().Any() ?? false)) .ValueOrDefault() }; if (files?.Count > 0) { ulong fileId = 0; List attachmentsNew = []; foreach (var file in files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, Filename = file.Filename }; attachmentsNew.Add(att); fileId++; } if (attachments.HasValue && attachments.Value.Any()) attachmentsNew.AddRange(attachments.Value); pld.Attachments = attachmentsNew; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in files.Where(x => x.ResetPositionTo.HasValue)) file.Stream.Position = file.ResetPositionTo.Value; return ret; } else { pld.Attachments = attachments.ValueOrDefault(); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } } /// /// Deletes the message async. /// /// The channel_id. /// The message_id. /// The reason. internal Task DeleteMessageAsync(ulong channelId, ulong messageId, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the messages async. /// /// The channel_id. /// The message_ids. /// The reason. internal Task DeleteMessagesAsync(ulong channelId, IEnumerable messageIds, string? reason) { var pld = new RestChannelMessageBulkDeletePayload { Messages = messageIds }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}{Endpoints.BULK_DELETE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the channel invites async. /// /// The channel_id. internal async Task> GetChannelInvitesAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invitesRaw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invitesRaw)); } /// /// Creates the channel invite async. /// /// The channel_id. /// The max_age. /// The max_uses. /// The target_type. /// The target_application. /// The target_user. /// If true, temporary. /// If true, unique. /// The reason. internal async Task CreateChannelInviteAsync(ulong channelId, int maxAge, int maxUses, TargetType? targetType, ulong? targetApplicationId, ulong? targetUser, bool temporary, bool unique, string? reason) { var pld = new RestChannelInviteCreatePayload { MaxAge = maxAge, MaxUses = maxUses, TargetType = targetType, TargetApplicationId = targetApplicationId, TargetUserId = targetUser, Temporary = temporary, Unique = unique }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } /// /// Deletes the channel permission async. /// /// The channel_id. /// The overwrite_id. /// The reason. internal Task DeleteChannelPermissionAsync(ulong channelId, ulong overwriteId, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(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 = channelId, overwrite_id = overwriteId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Edits the channel permissions async. /// /// The channel_id. /// The overwrite_id. /// The allow. /// The deny. /// The type. /// The reason. internal Task EditChannelPermissionsAsync(ulong channelId, ulong overwriteId, Permissions allow, Permissions deny, string type, string? reason) { var pld = new RestChannelPermissionEditPayload { Type = type, Allow = allow & PermissionMethods.FullPerms, Deny = deny & PermissionMethods.FullPerms }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id = channelId, overwrite_id = overwriteId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Triggers the typing async. /// /// The channel_id. internal Task TriggerTypingAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TYPING}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the pinned messages async. /// /// The channel_id. internal async Task> GetPinnedMessagesAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgsRaw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgsRaw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Pins the message async. /// /// The channel_id. /// The message_id. internal Task PinMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Unpins the message async. /// /// The channel_id. /// The message_id. internal Task UnpinMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the group dm recipient async. /// /// The channel_id. /// The user_id. /// The access_token. /// The nickname. internal Task AddGroupDmRecipientAsync(ulong channelId, ulong userId, string accessToken, string nickname) { var pld = new RestChannelGroupDmRecipientAddPayload { AccessToken = accessToken, Nickname = nickname }; 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 = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Removes the group dm recipient async. /// /// The channel_id. /// The user_id. internal Task RemoveGroupDmRecipientAsync(ulong channelId, ulong userId) { 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 = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Creates the group dm async. /// /// The access_tokens. /// The nicks. internal async Task CreateGroupDmAsync(IEnumerable accessTokens, IDictionary nicks) { var pld = new RestUserGroupDmCreatePayload { AccessTokens = accessTokens, Nicknames = nicks }; 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, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Creates the dm async. /// /// The recipient_id. internal async Task CreateDmAsync(ulong recipientId) { var pld = new RestUserDmCreatePayload { Recipient = recipientId }; 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, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Follows the channel async. /// /// The channel_id. /// The webhook_channel_id. internal async Task FollowChannelAsync(ulong channelId, ulong webhookChannelId) { var pld = new FollowedChannelAddPayload { WebhookChannelId = webhookChannelId }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.FOLLOWERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); return DiscordJson.DeserializeObject(response.Response, this.Discord); } /// /// Crossposts the message async. /// /// The channel_id. /// The message_id. internal async Task CrosspostMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.CROSSPOST}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route).ConfigureAwait(false); return DiscordJson.DeserializeObject(response.Response, this.Discord); } #endregion // End todo #region Member /// /// Gets the current user async. /// internal Task GetCurrentUserAsync() => this.GetUserAsync(Endpoints.ME); /// /// Gets the user async. /// /// The user_id. internal Task GetUserAsync(ulong userId) => this.GetUserAsync(userId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the user async. /// /// The user_id. internal async Task GetUserAsync(string userId) { var route = $"{Endpoints.USERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path.Replace("//", "/"), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var duser = DiscordJson.DeserializeObject(res.Response, this.Discord); if (this.Discord.Configuration.Intents.HasIntent(DiscordIntents.GuildPresences) && duser.Presence == null && this.Discord is DiscordClient dc) dc.PresencesInternal[duser.Id] = new() { Discord = dc, RawActivity = new(), Activity = new(), Status = UserStatus.Offline, InternalUser = new() { Id = duser.Id } }; return duser; } /// /// Gets the guild member async. /// /// The guild_id. /// The user_id. internal async Task GetGuildMemberAsync(ulong guildId, ulong userId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var tm = DiscordJson.DeserializeObject(res.Response, this.Discord); var usr = new DiscordUser(tm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(tm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.GlobalName = usr.GlobalName; return old; }); if (this.Discord.Configuration.Intents.HasIntent(DiscordIntents.GuildPresences) && usr.Presence == null && this.Discord is DiscordClient dc) dc.PresencesInternal[usr.Id] = new() { Discord = dc, RawActivity = new(), Activity = new(), Status = UserStatus.Offline }; return new(tm) { Discord = this.Discord, GuildId = guildId }; } /// /// Removes the guild member async. /// /// The guild_id. /// The user_id. /// The reason. internal Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string? 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 = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Modifies the current user async. /// /// The username. /// The base64_avatar. internal async Task ModifyCurrentUserAsync(string username, Optional base64Avatar) { var pld = new RestUserUpdateCurrentPayload { Username = username, AvatarBase64 = base64Avatar, AvatarSet = base64Avatar is { HasValue: true, Value: not null } }; var route = $"{Endpoints.USERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var user = DiscordJson.DeserializeObject(res.Response, this.Discord); return user; } /// /// Gets the current user guilds async. /// /// The limit. /// The before. /// The after. internal async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration) .AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); if (before != null) url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); if (after != null) url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); var res = await this.DoRequestAsync(this.Discord, bucket, url.Build(), RestRequestMethod.GET, route).ConfigureAwait(false); if (this.Discord is DiscordClient) { var guildsRaw = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); var glds = guildsRaw.Select(xug => (this.Discord as DiscordClient)?.GuildsInternal[xug.Id]); return new ReadOnlyCollection(new List(glds)); } else return DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); } /// /// Modifies the guild member async. /// /// The guild_id. /// The user_id. /// The nick. /// The role_ids. /// The mute. /// The deaf. /// The voice_channel_id. /// Whether to verify the member. /// The member flags /// The reason. internal Task ModifyGuildMemberAsync( ulong guildId, ulong userId, Optional nick, Optional> roleIds, Optional mute, Optional deaf, Optional voiceChannelId, Optional verify, MemberFlags flags, string? reason ) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick, RoleIds = roleIds, Deafen = deaf, Mute = mute, VoiceChannelId = voiceChannelId, Flags = verify is { HasValue: true, Value: true } ? flags | MemberFlags.BypassesVerification : verify is { HasValue: true, Value: false } ? flags & ~MemberFlags.BypassesVerification : Optional.None }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the time out of a guild member. /// /// The guild_id. /// The user_id. /// Datetime offset. /// The reason. internal Task ModifyTimeoutAsync(ulong guildId, ulong userId, DateTimeOffset? until, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberTimeoutModifyPayload { CommunicationDisabledUntil = until }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the current member nickname async. /// /// The guild_id. /// The nick. /// The reason. internal Task ModifyCurrentMemberNicknameAsync(ulong guildId, string nick, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.ME}{Endpoints.NICK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } #endregion #region Roles /// /// Gets the guild roles async. /// /// The guild_id. internal async Task> GetGuildRolesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var roles = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord).Select(xr => { xr.GuildId = guildId; return xr; }).ToList(); return roles; } /// /// Modifies the guild role async. /// /// The guild_id. /// The role_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The icon. /// The unicode emoji icon. /// The reason. internal async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, Optional iconb64, Optional emoji, string? reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FullPerms, Color = color, Hoist = hoist, Mentionable = mentionable }; if (emoji.HasValue && iconb64.HasValue) { pld.IconBase64 = null; pld.UnicodeEmoji = emoji; } else if (emoji.HasValue && !iconb64.HasValue) pld.UnicodeEmoji = emoji; else if (iconb64.HasValue && !emoji.HasValue) pld.IconBase64 = iconb64; 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 = guildId, role_id = roleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.GuildId = guildId; return ret; } /// /// Deletes the role async. /// /// The guild_id. /// The role_id. /// The reason. internal Task DeleteRoleAsync(ulong guildId, ulong roleId, string? reason) { 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.DELETE, route, new { guild_id = guildId, role_id = roleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Creates the guild role async. /// /// The guild_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The reason. internal async Task CreateGuildRoleAsync(ulong guildId, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string? reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FullPerms, Color = color, Hoist = hoist, Mentionable = mentionable }; 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 = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.GuildId = guildId; return ret; } #endregion #region Prune /// /// Gets the guild prune count async. /// /// The guild_id. /// The days. /// The include_roles. internal async Task GetGuildPruneCountAsync(ulong guildId, int days, IEnumerable includeRoles) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlParams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture) }; var sb = includeRoles?.Aggregate(new StringBuilder(), (sb, id) => sb.Append($"&include_roles={id}")) ?? new StringBuilder(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlParams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var pruned = DiscordJson.DeserializeObject(res.Response, this.Discord); return pruned.Pruned.Value; } /// /// Begins the guild prune async. /// /// The guild_id. /// The days. /// If true, compute_prune_count. /// The include_roles. /// The reason. internal async Task BeginGuildPruneAsync(ulong guildId, int days, bool computePruneCount, IEnumerable includeRoles, string? reason) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlParams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture), ["compute_prune_count"] = computePruneCount.ToString() }; var sb = includeRoles?.Aggregate(new StringBuilder(), (sb, id) => sb.Append($"&include_roles={id}")) ?? new StringBuilder(); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlParams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers).ConfigureAwait(false); var pruned = DiscordJson.DeserializeObject(res.Response, this.Discord); return pruned.Pruned; } #endregion #region GuildVarious /// /// Gets the template async. /// /// The code. internal async Task GetTemplateAsync(string code) { 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, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templatesRaw = DiscordJson.DeserializeObject(res.Response, this.Discord); return templatesRaw; } /// /// Gets the guild integrations async. /// /// The guild_id. internal async Task> GetGuildIntegrationsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var integrations = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return integrations; } /// /// Gets the guild preview async. /// /// The guild_id. internal async Task GetGuildPreviewAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PREVIEW}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Creates the guild integration async. /// /// The guild_id. /// The type. /// The id. internal async Task CreateGuildIntegrationAsync(ulong guildId, string type, ulong id) { var pld = new RestGuildIntegrationAttachPayload { Type = type, Id = id }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Modifies the guild integration async. /// /// The guild_id. /// The integration_id. /// The expire_behaviour. /// The expire_grace_period. /// If true, enable_emoticons. internal async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) { var pld = new RestGuildIntegrationModifyPayload { ExpireBehavior = expireBehaviour, ExpireGracePeriod = expireGracePeriod, EnableEmoticons = enableEmoticons }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, integration_id = integrationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Deletes the guild integration async. /// /// The guild_id. /// The integration. internal Task DeleteGuildIntegrationAsync(ulong guildId, DiscordIntegration integration) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id = guildId, integration_id = integration.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, payload: DiscordJson.SerializeObject(integration)); } /// /// Syncs the guild integration async. /// /// The guild_id. /// The integration_id. internal Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id{Endpoints.SYNC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId, integration_id = integrationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the guild voice regions async. /// /// The guild_id. internal async Task> GetGuildVoiceRegionsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return regions; } /// /// Gets the guild invites async. /// /// The guild_id. internal async Task> GetGuildInvitesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invites = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return invites; } #endregion #region Invite /// /// Gets the invite async. /// /// The invite_code. /// If true, with_counts. /// If true, with_expiration. /// The scheduled event id to get. internal async Task GetInviteAsync(string inviteCode, bool? withCounts, bool? withExpiration, ulong? guildScheduledEventId) { var urlParams = new Dictionary(); if (withCounts.HasValue) urlParams["with_counts"] = withCounts?.ToString(); if (withExpiration.HasValue) urlParams["with_expiration"] = withExpiration?.ToString(); if (guildScheduledEventId.HasValue) urlParams["guild_scheduled_event_id"] = guildScheduledEventId?.ToString(); var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { invite_code = inviteCode }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Deletes the invite async. /// /// The invite_code. /// The reason. internal async Task DeleteInviteAsync(string inviteCode, string? reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { invite_code = inviteCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /* * 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(this.Configuration), 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).ConfigureAwait(false); * * var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); * ret.Discord = this.Discord; * * return ret; * } */ #endregion #region Connections /// /// Gets the users connections async. /// internal async Task> GetUserConnectionsAsync() { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var connections = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return connections; } /// /// Gets the applications role connection metadata records. /// /// The application id. /// A list of metadata records or .s internal async Task> GetRoleConnectionMetadataRecords(ulong id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.ROLE_CONNECTIONS}{Endpoints.METADATA}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var metadata = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return metadata; } /// /// Updates the applications role connection metadata records. /// /// The application id. /// A list of metadata objects. Max 5. /// A list of the created metadata records.s internal async Task> UpdateRoleConnectionMetadataRecords(ulong id, IEnumerable metadataObjects) { var pld = new List(); foreach (var metadataObject in metadataObjects) pld.Add(new() { Type = metadataObject.Type, Key = metadataObject.Key, Name = metadataObject.Name, Description = metadataObject.Description, NameLocalizations = metadataObject.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = metadataObject.DescriptionLocalizations?.GetKeyValuePairs() }); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.ROLE_CONNECTIONS}{Endpoints.METADATA}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id = id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var metadata = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return metadata; } #endregion #region Voice /// /// Lists the voice regions async. /// internal async Task> ListVoiceRegionsAsync() { var route = $"{Endpoints.VOICE}{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return regions; } #endregion #region Webhooks /// /// Creates the webhook async. /// /// The channel_id. /// The name. /// The base64_avatar. /// The reason. internal async Task CreateWebhookAsync(ulong channelId, string name, Optional base64Avatar, string? reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.HasValue }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.ApiClient = this; return ret; } /// /// Gets the channel webhooks async. /// /// The channel_id. internal async Task> GetChannelWebhooksAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooksRaw = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord).Select(xw => { xw.ApiClient = this; return xw; }); return webhooksRaw.ToList(); } /// /// Gets the guild webhooks async. /// /// The guild_id. internal async Task> GetGuildWebhooksAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooksRaw = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord).Select(xw => { xw.ApiClient = this; return xw; }); return webhooksRaw.ToList(); } /// /// Gets the webhook async. /// /// The webhook_id. internal async Task GetWebhookAsync(ulong webhookId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord?.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.ApiClient = this; return ret; } /// /// Gets the webhook with token async. /// /// The webhook_id. /// The webhook_token. internal async Task GetWebhookWithTokenAsync(ulong webhookId, string webhookToken) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord?.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Token = webhookToken; ret.Id = webhookId; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The channel id. /// The name. /// The base64_avatar. /// The reason. internal async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, Optional base64Avatar, string? reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.HasValue, ChannelId = channelId }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The name. /// The base64_avatar. /// The webhook_token. /// The reason. internal async Task ModifyWebhookAsync(ulong webhookId, string name, string base64Avatar, string webhookToken, string? reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.ApiClient = this; return ret; } /// /// Deletes the webhook async. /// /// The webhook_id. /// The reason. internal Task DeleteWebhookAsync(ulong webhookId, string? reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The reason. internal Task DeleteWebhookAsync(ulong webhookId, string webhookToken, string? reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Executes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The builder. /// The thread_id. internal async Task ExecuteWebhookAsync(ulong webhookId, string webhookToken, DiscordWebhookBuilder builder, string threadId) { builder.Validate(); 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 flags = MessageFlags.None; if (builder.EmbedsSuppressed) flags |= MessageFlags.SuppressedEmbeds; if (builder.NotificationsSuppressed) flags |= MessageFlags.SuppressNotifications; var pld = new RestWebhookExecutePayload { Content = builder.Content, Username = builder.Username.ValueOrDefault(), AvatarUrl = builder.AvatarUrl.ValueOrDefault(), IsTts = builder.IsTts, Embeds = builder.Embeds, Components = builder.Components, ThreadName = builder.ThreadName, Flags = flags, AppliedTags = builder.AppliedTags.Any() ? builder.AppliedTags : null }; if (builder.Mentions != null) pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); if (builder.Files?.Count > 0) { ulong fileId = 0; List attachments = []; foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, Filename = file.Filename, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; } if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.Files?.Count > 0 || builder.IsTts == true || builder.Mentions != null) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord?.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); if (this.Discord != null!) foreach (var att in ret.Attachments) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) file.Stream.Position = file.ResetPositionTo.Value; return ret; } /// /// Executes the webhook slack async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. internal async Task ExecuteWebhookSlackAsync(ulong webhookId, string webhookToken, string jsonPayload, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.SLACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: jsonPayload).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Executes the webhook github async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. internal async Task ExecuteWebhookGithubAsync(ulong webhookId, string webhookToken, string jsonPayload, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.GITHUB}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: jsonPayload).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. internal async Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, DiscordWebhookBuilder builder, string threadId) { builder.Validate(true); MessageFlags? flags = builder.FlagsChanged ? MessageFlags.None : null; if (builder.EmbedsSuppressed) flags |= MessageFlags.SuppressedEmbeds; if (builder.NotificationsSuppressed) flags |= MessageFlags.SuppressNotifications; var pld = new RestWebhookMessageEditPayload { Content = builder.Content, Embeds = builder.Embeds, Mentions = builder.Mentions, Components = builder.Components, Flags = flags }; if (builder.Files?.Count > 0) { ulong fileId = 0; List attachments = []; foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, Filename = file.Filename, FileSize = null }; attachments.Add(att); fileId++; } if (builder.Attachments is { Count: > 0 }) attachments.AddRange(builder.Attachments); pld.Attachments = attachments; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); foreach (var att in ret.AttachmentsInternal) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) file.Stream.Position = file.ResetPositionTo.Value; return ret; } else { pld.Attachments = builder.Attachments; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); foreach (var att in ret.AttachmentsInternal) att.Discord = this.Discord; return ret; } } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. internal Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, DiscordWebhookBuilder builder, ulong threadId) => this.EditWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), builder, threadId.ToString()); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal async Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. internal Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) => this.GetWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), null); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, ulong threadId) => this.GetWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), threadId.ToString()); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal async Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); } /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. internal Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) => this.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), null); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, ulong threadId) => this.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), threadId.ToString()); #endregion #region Reactions /// /// Creates the reaction async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task CreateReactionAsync(ulong channelId, ulong messageId, string emoji) { 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 = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } /// /// Deletes the own reaction async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task DeleteOwnReactionAsync(ulong channelId, ulong messageId, 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 = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } /// /// Deletes the user reaction async. /// /// The channel_id. /// The message_id. /// The user_id. /// The emoji. /// The reason. internal Task DeleteUserReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, string? reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; 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 = channelId, message_id = messageId, emoji, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } /// /// Gets the reactions async. /// /// The channel_id. /// The message_id. /// The emoji. /// The after_id. /// The limit. internal async Task> GetReactionsAsync(ulong channelId, ulong messageId, string emoji, ulong? afterId = null, int limit = 25) { var urlParams = new Dictionary(); if (afterId.HasValue) urlParams["after"] = afterId.Value.ToString(CultureInfo.InvariantCulture); urlParams["limit"] = limit.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 = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var reactersRaw = JsonConvert.DeserializeObject>(res.Response); var reacters = new List(); foreach (var xr in reactersRaw) { var usr = new DiscordUser(xr) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.GlobalName = usr.GlobalName; return old; }); reacters.Add(usr); } return new ReadOnlyCollection(new List(reacters)); } /// /// Deletes the all reactions async. /// /// The channel_id. /// The message_id. /// The reason. internal Task DeleteAllReactionsAsync(ulong channelId, ulong messageId, string? reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } /// /// Deletes the reactions emoji async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task DeleteReactionsEmojiAsync(ulong channelId, ulong messageId, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } #endregion #region Threads /// /// Creates the thread. /// /// The channel id to create the thread in. /// The optional message id to create the thread from. /// The name of the thread. /// The auto_archive_duration for the thread. /// Can be either or . /// The rate limit per user. /// The tags to add on creation. /// The message builder. /// Whether this thread is in a forum. /// The reason. internal async Task CreateThreadAsync( ulong channelId, ulong? messageId, string name, ThreadAutoArchiveDuration? autoArchiveDuration, ChannelType? type, int? rateLimitPerUser, IEnumerable? appliedTags = null, DiscordMessageBuilder builder = null, bool isForum = false, string? reason = null ) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = autoArchiveDuration, PerUserRateLimit = rateLimitPerUser }; if (isForum) { pld.Message = new() { Content = builder.Content, Attachments = builder.Attachments, Components = builder.Components, HasContent = true, Embeds = builder.Embeds, //Flags = builder.Flags, //Mentions = builder.Mentions, StickersIds = builder.Sticker != null ? new List(1) { builder.Sticker.Id } : null }; if (appliedTags != null && appliedTags.Any()) { List tags = []; foreach (var b in appliedTags) tags.Add(b.Id.Value); pld.AppliedTags = tags; pld.Type = null; } } else pld.Type = type; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; if (messageId is not null) route += $"{Endpoints.MESSAGES}/:message_id"; route += Endpoints.THREADS; object param = messageId is null ? new { channel_id = channelId } : new { channel_id = channelId, message_id = messageId }; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, param, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var threadChannel = DiscordJson.DeserializeObject(res.Response, this.Discord); return threadChannel; } /// /// Gets the thread. /// /// The thread id. internal async Task GetThreadAsync(ulong threadId) { var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { thread_id = threadId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Joins the thread. /// /// The channel id. internal async Task JoinThreadAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route).ConfigureAwait(false); } /// /// Leaves the thread. /// /// The channel id. internal async Task LeaveThreadAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); } /// /// Adds a thread member. /// /// The channel id to add the member to. /// The user id to add. internal async Task AddThreadMemberAsync(ulong channelId, ulong userId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route).ConfigureAwait(false); } /// /// Gets a thread member. /// /// The channel id to get the member from. /// The user id to get. /// Whether to include a object. internal async Task GetThreadMemberAsync(ulong channelId, ulong userId, bool withMember = false) { var urlParams = new Dictionary { ["with_member"] = withMember.ToString(CultureInfo.InvariantCulture) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var threadMember = DiscordJson.DeserializeObject(res.Response, this.Discord); return threadMember; } /// /// Removes a thread member. /// /// The channel id to remove the member from. /// The user id to remove. internal async Task RemoveThreadMemberAsync(ulong channelId, ulong userId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); } /// /// Gets the thread members. /// /// The thread id. /// Whether to include a object. /// Get members after specified snowflake. /// Limits the results. internal async Task> GetThreadMembersAsync(ulong threadId, bool withMember = false, ulong? after = null, int? limit = null) { // TODO: Starting in API v11, List Thread Members will always return paginated results, regardless of whether with_member is passed or not. var urlParams = new Dictionary { ["with_member"] = withMember.ToString(CultureInfo.InvariantCulture) }; if (after != null && withMember) urlParams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); if (limit is > 0 && withMember) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:thread_id{Endpoints.THREAD_MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { thread_id = threadId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var tm = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return tm; } /// /// Gets the active threads in a guild. /// /// The guild id. internal async Task GetActiveThreadsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.THREADS}{Endpoints.THREAD_ACTIVE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var threadReturn = DiscordJson.DeserializeObject(res.Response, this.Discord); threadReturn.Threads.ForEach(x => x.Discord = this.Discord); return threadReturn; } /// /// Gets the joined private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetJoinedPrivateArchivedThreadsAsync(ulong channelId, ulong? before, int? limit) { var urlParams = new Dictionary(); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit is > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.USERS}{Endpoints.ME}{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var threadReturn = DiscordJson.DeserializeObject(res.Response, this.Discord); return threadReturn; } /// /// Gets the public archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPublicArchivedThreadsAsync(ulong channelId, ulong? before, int? limit) { var urlParams = new Dictionary(); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit is > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PUBLIC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var threadReturn = DiscordJson.DeserializeObject(res.Response, this.Discord); return threadReturn; } /// /// Gets the private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPrivateArchivedThreadsAsync(ulong channelId, ulong? before, int? limit) { var urlParams = new Dictionary(); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit is > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var threadReturn = DiscordJson.DeserializeObject(res.Response, this.Discord); return threadReturn; } /// /// Modifies a thread. /// /// The thread to modify. /// The parent channels type as failback to ignore forum fields. /// The new name. /// The new locked state. /// The new archived state. /// The new per user rate limit. /// The new auto archive duration. /// The new user invitable state. /// The tags to add on creation. /// Whether the post is pinned. /// The reason for the modification. internal Task ModifyThreadAsync(ulong threadId, ChannelType parentType, string name, Optional locked, Optional archived, Optional perUserRateLimit, Optional autoArchiveDuration, Optional invitable, Optional> appliedTags, Optional pinned, string? reason) { var pld = new RestThreadChannelModifyPayload { Name = name, Archived = archived, AutoArchiveDuration = autoArchiveDuration, Locked = locked, PerUserRateLimit = perUserRateLimit, Invitable = invitable }; if (parentType == ChannelType.Forum) { if (appliedTags is { HasValue: true, Value: not null }) { List tags = new(appliedTags.Value.Count()); foreach (var b in appliedTags.Value) tags.Add(b.Id.Value); pld.AppliedTags = tags; } if (pinned is { HasValue: true, Value: not null }) pld.Flags = pinned.Value.Value ? ChannelFlags.Pinned : Optional.None; } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { thread_id = threadId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } #endregion #region Emoji /// /// Gets the guild emojis async. /// /// The guild_id. internal async Task> GetGuildEmojisAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var emojisRaw = JsonConvert.DeserializeObject>(res.Response); this.Discord.Guilds.TryGetValue(guildId, out var gld); var users = new Dictionary(); var emojis = new List(); foreach (var rawEmoji in emojisRaw) { var xge = rawEmoji.ToObject(); xge.Guild = gld; var xtu = rawEmoji["user"]?.ToObject(); if (xtu != null) { 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]; } emojis.Add(xge); } return new ReadOnlyCollection(emojis); } /// /// Gets the guild emoji async. /// /// The guild_id. /// The emoji_id. internal async Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Creates the guild emoji async. /// /// The guild_id. /// The name. /// The imageb64. /// The roles. /// The reason. internal async Task CreateGuildEmojiAsync(ulong guildId, string name, string imageb64, IEnumerable roles, string? reason) { 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 = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); emoji.User = xtu != null ? gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu) : this.Discord.CurrentUser; return emoji; } /// /// Modifies the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The name. /// The roles. /// The reason. internal async Task ModifyGuildEmojiAsync(ulong guildId, ulong emojiId, string name, IEnumerable roles, string? reason) { var pld = new RestGuildEmojiModifyPayload { Name = name, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Deletes the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The reason. internal Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string? reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Stickers /// /// Gets a sticker. /// /// The sticker id. internal async Task GetStickerAsync(ulong stickerId) { var route = $"{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { sticker_id = stickerId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Gets the sticker packs. /// internal async Task> GetStickerPacksAsync() { var route = $"{Endpoints.STICKERPACKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response)["sticker_packs"] as JArray; var ret = json.ToDiscordObject(); return ret.ToList(); } /// /// Gets the guild stickers. /// /// The guild id. internal async Task> GetGuildStickersAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return ret; } /// /// Gets a guild sticker. /// /// The guild id. /// The sticker id. internal async Task GetGuildStickerAsync(ulong guildId, ulong stickerId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId, sticker_id = stickerId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Creates the guild sticker. /// /// The guild id. /// The name. /// The description. /// The tags. /// The file. /// The reason. internal async Task CreateGuildStickerAsync(ulong guildId, string name, string description, string tags, DiscordMessageFile file, string? reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var res = await this.DoStickerMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, name, tags, description, headers, file).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Modifies the guild sticker. /// /// The guild id. /// The sticker id. /// The name. /// The description. /// The tags. /// The reason. internal async Task ModifyGuildStickerAsync(ulong guildId, ulong stickerId, Optional name, Optional description, Optional tags, string? reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId, sticker_id = stickerId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var pld = new RestStickerModifyPayload() { Name = name, Description = description, Tags = tags }; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); return ret; } /// /// Deletes the guild sticker async. /// /// The guild id. /// The sticker id. /// The reason. internal async Task DeleteGuildStickerAsync(ulong guildId, ulong stickerId, string? reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id = guildId, sticker_id = stickerId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers).ConfigureAwait(false); } #endregion #region Application Commands /// /// Gets the global application commands. /// /// The application id. /// Whether to get the full localization dict. internal async Task> GetGlobalApplicationCommandsAsync(ulong applicationId, bool withLocalizations = false) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = applicationId }, out var path); var querydict = new Dictionary { ["with_localizations"] = withLocalizations.ToString().ToLower() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return ret.ToList(); } /// /// Bulk overwrites the global application commands. /// /// The application id. /// The commands. internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong applicationId, IEnumerable commands) { var pld = new List(); if (commands.Any()) pld.AddRange(commands.Select(command => new RestApplicationCommandCreatePayload() { Type = command.Type, Name = command.Name, Description = command.Type is ApplicationCommandType.ChatInput ? command.Description : null, Options = command.Options, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission, Nsfw = command.IsNsfw, AllowedContexts = command.AllowedContexts, IntegrationTypes = command.IntegrationTypes })); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return ret.ToList(); } /// /// Creates a global application command. /// /// The applicationid. /// The command. internal async Task CreateGlobalApplicationCommandAsync(ulong applicationId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Type is ApplicationCommandType.ChatInput ? command.Description : null, Options = command.Options, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission, Nsfw = command.IsNsfw, AllowedContexts = command.AllowedContexts, IntegrationTypes = command.IntegrationTypes }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } /// /// Gets a global application command. /// /// The application id. /// The command id. internal async Task GetGlobalApplicationCommandAsync(ulong applicationId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } /// /// Edits a global application command. /// /// The application id. /// The command id. /// The name. /// The description. /// The options. /// The localizations of the name. /// The localizations of the description. /// The default member permissions. /// The dm permission. /// Whether this command is marked as NSFW. /// The allowed contexts. /// The allowed integration types. internal async Task EditGlobalApplicationCommandAsync( ulong applicationId, ulong commandId, Optional name, Optional description, Optional?> options, Optional nameLocalization, Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission, Optional isNsfw, - Optional?> allowedContexts, + Optional?> allowedContexts, Optional?> integrationTypes ) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultMemberPermission = defaultMemberPermission, DmPermission = dmPermission, NameLocalizations = nameLocalization.ValueOrDefault()?.GetKeyValuePairs(), DescriptionLocalizations = descriptionLocalization.ValueOrDefault()?.GetKeyValuePairs(), Nsfw = isNsfw, AllowedContexts = allowedContexts, IntegrationTypes = integrationTypes }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } /// /// Deletes a global application command. /// /// The application_id. /// The command_id. internal async Task DeleteGlobalApplicationCommandAsync(ulong applicationId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); } /// /// Gets the guild application commands. /// /// The application id. /// The guild id. /// Whether to get the full localization dict. internal async Task> GetGuildApplicationCommandsAsync(ulong applicationId, ulong guildId, bool withLocalizations = false) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = applicationId, guild_id = guildId }, out var path); var querydict = new Dictionary { ["with_localizations"] = withLocalizations.ToString().ToLower() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return ret.ToList(); } /// /// Bulk overwrites the guild application commands. /// /// The application id. /// The guild id. /// The commands. internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong applicationId, ulong guildId, IEnumerable commands) { var pld = new List(); if (commands.Any()) pld.AddRange(commands.Select(command => new RestApplicationCommandCreatePayload() { Type = command.Type, Name = command.Name, Description = command.Type is ApplicationCommandType.ChatInput ? command.Description : null, Options = command.Options, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission, Nsfw = command.IsNsfw, AllowedContexts = command.AllowedContexts })); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id = applicationId, guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); return ret.ToList(); } /// /// Creates a guild application command. /// /// The application id. /// The guild id. /// The command. internal async Task CreateGuildApplicationCommandAsync(ulong applicationId, ulong guildId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Type == ApplicationCommandType.ChatInput ? command.Description : null, Options = command.Options, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission, Nsfw = command.IsNsfw, AllowedContexts = command.AllowedContexts }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id = applicationId, guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } /// /// Gets a guild application command. /// /// The application id. /// The guild id. /// The command id. internal async Task GetGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId) { 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 = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } /// /// Edits a guild application command. /// /// The application id. /// The guild id. /// The command id. /// The name. /// The description. /// The options. /// The localizations of the name. /// The localizations of the description. /// The default member permissions. /// The dm permission. /// Whether this command is marked as NSFW. /// The allowed contexts. /// The allowed integration types. internal async Task EditGuildApplicationCommandAsync( ulong applicationId, ulong guildId, ulong commandId, Optional name, Optional description, Optional?> options, Optional nameLocalization, Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission, Optional isNsfw, - Optional?> allowedContexts, + Optional?> allowedContexts, Optional?> integrationTypes ) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultMemberPermission = defaultMemberPermission, DmPermission = dmPermission, NameLocalizations = nameLocalization.ValueOrDefault()?.GetKeyValuePairs(), DescriptionLocalizations = descriptionLocalization.ValueOrDefault()?.GetKeyValuePairs(), Nsfw = isNsfw, AllowedContexts = allowedContexts, IntegrationTypes = integrationTypes }; 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 = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } /// /// Deletes a guild application command. /// /// The application id. /// The guild id. /// The command id. internal async Task DeleteGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId) { 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 = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); } /// /// Creates an interaction response. /// /// The interaction id. /// The interaction token. /// The type. /// The builder. internal async Task CreateInteractionResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, DiscordInteractionResponseBuilder builder) { if (builder?.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); RestInteractionResponsePayload pld; if (type != InteractionResponseType.AutoCompleteResult) { MessageFlags? flags = builder is { FlagsChanged: true } ? MessageFlags.None : null; if (builder != null) { if (builder.IsEphemeral) flags |= MessageFlags.Ephemeral; if (builder.EmbedsSuppressed) flags |= MessageFlags.SuppressedEmbeds; if (builder.NotificationsSuppressed) flags |= MessageFlags.SuppressNotifications; } var data = builder != null ? new DiscordInteractionApplicationCommandCallbackData { Content = builder?.Content ?? null, Embeds = builder?.Embeds ?? null, IsTts = builder?.IsTts, Mentions = builder?.Mentions ?? null, Flags = flags, Components = builder?.Components ?? null, Choices = null } : null; pld = new() { Type = type, Data = data, CallbackHints = builder?.CallbackHints }; if (builder is { Files.Count: > 0 }) { ulong fileId = 0; List attachments = []; foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, Filename = file.Filename, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; pld.Data.Attachments = attachments; } } else pld = new() { Type = type, Data = new() { Content = null, Embeds = null, IsTts = null, Mentions = null, Flags = null, Components = null, Choices = builder.Choices, Attachments = null }, Attachments = null, CallbackHints = null }; var values = new Dictionary(); if (builder != null) if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts == true || builder.Mentions != null || builder.Files?.Count > 0 || builder.Components?.Count > 0) 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 = interactionId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "false").Build(); if (builder != null && values.Count is not 0) { await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) file.Stream.Position = file.ResetPositionTo.Value; } else await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Creates an interaction response with a modal. /// /// The interaction id. /// The interaction token. /// The type. /// The builder. internal async Task CreateInteractionModalResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, DiscordInteractionModalBuilder builder) { if (builder.ModalComponents.Any(mc => mc.Components.Any(c => c.Type != ComponentType.InputText))) throw new NotSupportedException("Can't send any other type then Input Text as Modal Component."); var pld = new RestInteractionModalResponsePayload { Type = type, Data = new() { Title = builder.Title, CustomId = builder.CustomId, ModalComponents = builder.ModalComponents }, CallbackHints = builder?.CallbackHints }; var values = new Dictionary(); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { interaction_id = interactionId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Creates an interaction response with an iFrame. /// /// The interaction id. /// The interaction token. /// The type. /// The custom id of the iframe. /// The title of the iframe. /// The size of the iframe. /// The path of the iframe. internal async Task CreateInteractionIframeResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, string customId, string title, IframeModalSize modalSize, string? iFramePath = null) { var pld = new RestInteractionIframeResponsePayload { Type = type, Data = new() { Title = title, CustomId = customId, ModalSize = modalSize, IframePath = iFramePath }, CallbackHints = null }; var values = new Dictionary(); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { interaction_id = interactionId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Gets the original interaction response. /// /// The application id. /// The interaction token. internal Task GetOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.GetWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Edits the original interaction response. /// /// The application id. /// The interaction token. /// The builder. internal Task EditOriginalInteractionResponseAsync(ulong applicationId, string interactionToken, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, builder, null); /// /// Deletes the original interaction response. /// /// The application id. /// The interaction token. internal Task DeleteOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Creates the followup message. /// /// The application id. /// The interaction token. /// The builder. internal async Task CreateFollowupMessageAsync(ulong applicationId, string interactionToken, DiscordFollowupMessageBuilder builder) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); MessageFlags? flags = builder is { FlagsChanged: true } ? MessageFlags.None : null; if (builder.IsEphemeral) flags |= MessageFlags.Ephemeral; if (builder.EmbedsSuppressed) flags |= MessageFlags.SuppressedEmbeds; if (builder.NotificationsSuppressed) flags |= MessageFlags.SuppressNotifications; var values = new Dictionary(); var pld = new RestFollowupMessageCreatePayload { Content = builder.Content, IsTts = builder.IsTts, Embeds = builder.Embeds, Flags = flags, Components = builder.Components }; if (builder.Files is { Count: > 0 }) { ulong fileId = 0; List attachments = []; foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, Filename = file.Filename, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; } if (builder.Mentions != null) pld.Mentions = new(builder.Mentions, builder.Mentions.Count is not 0); if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts == true || builder.Mentions != null || builder.Files?.Count > 0 || builder.Components?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:application_id/:interaction_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id = applicationId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); RestResponse res; if (values.Count is not 0) { res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); foreach (var att in ret.AttachmentsInternal) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) file.Stream.Position = file.ResetPositionTo.Value; ret.Discord = this.Discord; return ret; } else { res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = DiscordJson.DeserializeObject(res.Response, this.Discord); ret.Discord = this.Discord; return ret; } } /// /// Gets the followup message. /// /// The application id. /// The interaction token. /// The message id. internal Task GetFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.GetWebhookMessageAsync(applicationId, interactionToken, messageId); /// /// Edits the followup message. /// /// The application id. /// The interaction token. /// The message id. /// The builder. internal Task EditFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, messageId.ToString(), builder, null); /// /// Deletes the followup message. /// /// The application id. /// The interaction token. /// The message id. internal Task DeleteFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, messageId); #endregion #region Misc /// /// Gets the published store sku listings (premium application subscription). /// /// The application id to fetch the listenings for. /// A list of published listings with s. internal async Task> GetPublishedListingsAsync(ulong applicationId) { var urlParams = new Dictionary { ["application_id"] = applicationId.ToString() }; var route = $"{Endpoints.STORE}{Endpoints.PUBLISHED_LISTINGS}{Endpoints.SKUS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var storeSkus = DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord); foreach (var storeSku in storeSkus) storeSku.Sku.Discord = this.Discord; return new ReadOnlyCollection(storeSkus); } /// /// Gets the applications skus. /// /// The application id to fetch the listenings for. /// A list of published listings with s. internal async Task> GetSkusAsync(ulong applicationId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.SKUS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); return new ReadOnlyCollection(DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord)); } /// /// Gets the applications entitlements. /// /// The application id to fetch the entitlement for. /// Filter returned entitlements to a specific guild id. /// Filter returned entitlements to a specific user id. /// A list of . internal async Task> GetEntitlementsAsync(ulong applicationId, ulong? guildId, ulong? userId) { var urlParams = new Dictionary(); if (guildId.HasValue) urlParams["guild_id"] = guildId.Value.ToString(); if (userId.HasValue) urlParams["user_id"] = userId.Value.ToString(); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.ENTITLEMENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); return new ReadOnlyCollection(DiscordJson.DeserializeIEnumerableObject>(res.Response, this.Discord)); } /// /// Creates a test entitlement. /// /// The application id to create the entitlement for. /// The sku id to create the entitlement for. /// The owner id to create the entitlement for. /// The owner type to create the entitlement for. /// A partial . internal async Task CreateTestEntitlementsAsync(ulong applicationId, ulong skuId, ulong ownerId, EntitlementOwnerType ownerType) { TestEntitlementCreatePayload pld = new() { SkuId = skuId, OwnerId = ownerId, OwnerType = ownerType }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.ENTITLEMENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); return DiscordJson.DeserializeObject(res.Response, this.Discord); } /// /// Deletes a test entitlement. /// /// The application id to delete the entitlement for. /// The entitlement id to delete. internal async Task DeleteTestEntitlementsAsync(ulong applicationId, ulong entitlementId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.ENTITLEMENTS}/:entitlement_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id = applicationId, entitlement_id = entitlementId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); } /// /// Gets the current application info via oauth2. /// internal Task GetCurrentApplicationOauth2InfoAsync() => this.GetApplicationOauth2InfoAsync(Endpoints.ME); /// /// Gets the application rpc info. /// /// The application_id. internal Task GetApplicationRpcInfoAsync(ulong applicationId) => this.GetApplicationRpcInfoAsync(applicationId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the application info via oauth2. /// /// The application_id. private async Task GetApplicationOauth2InfoAsync(string applicationId) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path.Replace("//", "/"), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var app = DiscordJson.DeserializeObject(res.Response, this.Discord); return app; } /// /// Gets the application info. /// internal async Task GetCurrentApplicationInfoAsync() { var route = $"{Endpoints.APPLICATIONS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var tapp = DiscordJson.DeserializeObject(res.Response, this.Discord); if (tapp.Guild.HasValue) tapp.Guild.Value.Discord = this.Discord; return tapp; } /// /// Gets the application info. /// internal async Task ModifyCurrentApplicationInfoAsync( Optional description, Optional interactionsEndpointUrl, Optional roleConnectionsVerificationUrl, Optional customInstallUrl, Optional?> tags, Optional iconb64, Optional coverImageb64, Optional flags, Optional installParams, Optional integrationTypesConfig ) { var pld = new RestApplicationModifyPayload() { Description = description, InteractionsEndpointUrl = interactionsEndpointUrl, RoleConnectionsVerificationUrl = roleConnectionsVerificationUrl, CustomInstallUrl = customInstallUrl, Tags = tags, IconBase64 = iconb64, ConverImageBase64 = coverImageb64, Flags = flags, InstallParams = installParams, IntegrationTypesConfig = integrationTypesConfig }; var route = $"{Endpoints.APPLICATIONS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var app = DiscordJson.DeserializeObject(res.Response, this.Discord); app.Discord = this.Discord; return app; } /// /// Gets the application info. /// /// The application_id. private async Task GetApplicationRpcInfoAsync(string applicationId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.RPC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); return DiscordJson.DeserializeObject(res.Response, this.Discord); } /// /// Gets the application assets async. /// /// The application. 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); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var assets = JsonConvert.DeserializeObject>(res.Response); foreach (var asset in assets) { asset.Discord = application.Discord; asset.Application = application; } return new ReadOnlyCollection(new List(assets)); } /// /// Gets the gateway info async. /// internal async Task GetGatewayInfoAsync() { 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); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, headers).ConfigureAwait(false); var info = JObject.Parse(res.Response).ToObject(); info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); return info; } internal async Task RequestFileUploadAsync(ulong channelId, GcpAttachment attachment) { var pld = new RestGcpAttachmentsPayload { GcpAttachments = [attachment] }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.ATTACHMENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); return DiscordJson.DeserializeObject(res.Response, this.Discord); } internal void UploadGcpFile(GcpAttachmentUploadInformation target, Stream file) { HttpRequestMessage request = new(HttpMethod.Put, target.UploadUrl) { Content = new StreamContent(file) }; this.Rest.HttpClient.Send(request); } #endregion #region OAuth2 /// /// Gets the current oauth2 authorization information. /// /// The access token. internal async Task GetCurrentOAuth2AuthorizationInformationAsync(string accessToken) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.OAUTH2}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var headers = Utilities.GetBaseHeaders(); headers.Add("Bearer", accessToken); var url = Utilities.GetApiUriFor(path); var res = await this.DoRequestAsync(null, bucket, url, RestRequestMethod.GET, route, headers).ConfigureAwait(false); var oauth2Info = DiscordJson.DeserializeObject(res.Response, null); return oauth2Info; } /// /// Gets the current user. /// /// The access token. internal async Task GetCurrentUserAsync(string accessToken) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.USERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = this.OAuth2Client.ClientId.ToString(CultureInfo.InvariantCulture) }, out var path); var headers = Utilities.GetBaseHeaders(); headers.Add("Bearer", accessToken); var url = Utilities.GetApiUriFor(path); var res = await this.DoRequestAsync(null, bucket, url, RestRequestMethod.GET, route, headers) .ConfigureAwait(false); var tuser = DiscordJson.DeserializeObject(res.Response, null); return new(tuser); } /// /// Gets the current user's connections. /// /// The access token. internal async Task> GetCurrentUserConnectionsAsync(string accessToken) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var headers = Utilities.GetBaseHeaders(); headers.Add("Bearer", accessToken); var url = Utilities.GetApiUriFor(path); var res = await this.DoRequestAsync(null, bucket, url, RestRequestMethod.GET, route, headers) .ConfigureAwait(false); return DiscordJson.DeserializeIEnumerableObject>(res.Response, null); } /// /// Gets the current user's guilds. /// /// The access token. internal async Task> GetCurrentUserGuildsAsync(string accessToken) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var headers = Utilities.GetBaseHeaders(); headers.Add("Bearer", accessToken); var url = Utilities.GetApiUriFor(path); var res = await this.DoRequestAsync(null, bucket, url, RestRequestMethod.GET, route, headers) .ConfigureAwait(false); return DiscordJson.DeserializeIEnumerableObject>(res.Response, null); } /// /// Gets the current user's guilds. /// /// The access token. /// The guild id to get the member for. internal async Task GetCurrentUserGuildMemberAsync(string accessToken, ulong guildId) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId.ToString(CultureInfo.InvariantCulture) }, out var path); var headers = Utilities.GetBaseHeaders(); headers.Add("Bearer", accessToken); var url = Utilities.GetApiUriFor(path); var res = await this.DoRequestAsync(null, bucket, url, RestRequestMethod.GET, route, headers) .ConfigureAwait(false); var tmember = DiscordJson.DeserializeObject(res.Response, null); return new(tmember); } /// /// Gets the current user's role connection. /// /// The access token. internal async Task GetCurrentUserApplicationRoleConnectionAsync(string accessToken) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.APPLICATIONS}/:application_id/{Endpoints.ROLE_CONNECTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = this.OAuth2Client.ClientId.ToString(CultureInfo.InvariantCulture) }, out var path); var headers = Utilities.GetBaseHeaders(); headers.Add("Bearer", accessToken); var url = Utilities.GetApiUriFor(path); var res = await this.DoRequestAsync(null, bucket, url, RestRequestMethod.GET, route, headers) .ConfigureAwait(false); return DiscordJson.DeserializeObject(res.Response, null); } /// /// Updates the current user's role connection. /// /// The access token. /// The platform name. /// The platform username. /// The metadata. internal Task ModifyCurrentUserApplicationRoleConnectionAsync(string accessToken, Optional platformName, Optional platformUsername, Optional metadata) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); RestOAuth2ApplicationRoleConnectionPayload pld = new() { PlatformName = platformName, PlatformUsername = platformUsername, Metadata = metadata.HasValue ? metadata.Value.GetKeyValuePairs() : Optional>.None }; // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.APPLICATIONS}/:application_id/{Endpoints.ROLE_CONNECTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id = this.OAuth2Client.ClientId.ToString(CultureInfo.InvariantCulture) }, out var path); var headers = Utilities.GetBaseHeaders(); headers.Add("Bearer", accessToken); var url = Utilities.GetApiUriFor(path); this.DoRequestAsync(null, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); return Task.CompletedTask; } /// /// Exchanges a code for an access token. /// /// The code. internal async Task ExchangeOAuth2AccessTokenAsync(string code) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.OAUTH2}{Endpoints.TOKEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var formData = new Dictionary { { "client_id", this.OAuth2Client.ClientId.ToString(CultureInfo.InvariantCulture) }, { "client_secret", this.OAuth2Client.ClientSecret }, { "grant_type", "authorization_code" }, { "code", code }, { "redirect_uri", this.OAuth2Client.RedirectUri.AbsoluteUri } }; var url = Utilities.GetApiUriFor(path); var res = await this.DoFormRequestAsync(this.OAuth2Client, bucket, url, RestRequestMethod.POST, route, formData).ConfigureAwait(false); var accessTokenInformation = DiscordJson.DeserializeObject(res.Response, null); return accessTokenInformation; } /// /// Exchanges a refresh token for a new access token /// /// The refresh token. internal async Task RefreshOAuth2AccessTokenAsync(string refreshToken) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.OAUTH2}{Endpoints.TOKEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var formData = new Dictionary { { "client_id", this.OAuth2Client.ClientId.ToString(CultureInfo.InvariantCulture) }, { "client_secret", this.OAuth2Client.ClientSecret }, { "grant_type", "refresh_token" }, { "refresh_token", refreshToken }, { "redirect_uri", this.OAuth2Client.RedirectUri.AbsoluteUri } }; var url = Utilities.GetApiUriFor(path); var res = await this.DoFormRequestAsync(this.OAuth2Client, bucket, url, RestRequestMethod.POST, route, formData).ConfigureAwait(false); var accessTokenInformation = DiscordJson.DeserializeObject(res.Response, null); return accessTokenInformation; } /// /// Revokes an oauth2 token. /// /// The token to revoke. /// The type of token to revoke. internal Task RevokeOAuth2TokenAsync(string token, string type) { if (this.Discord != null!) throw new InvalidOperationException("Cannot use oauth2 endpoints with discord client"); // ReSharper disable once HeuristicUnreachableCode var route = $"{Endpoints.OAUTH2}{Endpoints.TOKEN}{Endpoints.REVOKE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var authorizationString = Encoding.UTF8.GetBytes($"{this.OAuth2Client.ClientId.ToString(CultureInfo.InvariantCulture)}:{this.OAuth2Client.ClientSecret}"); var base64EncodedAuthorizationString = Convert.ToBase64String(authorizationString); var headers = Utilities.GetBaseHeaders(); headers.Add("Basic", base64EncodedAuthorizationString); var formData = new Dictionary { { "token", token }, { "token_type_hint", type } }; var url = Utilities.GetApiUriFor(path); this.DoFormRequestAsync(this.OAuth2Client, bucket, url, RestRequestMethod.POST, route, formData, headers).ConfigureAwait(false); return Task.CompletedTask; } #endregion }