diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 594a0f370..e0d10af87 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -26,5 +26,10 @@ namespace Discord /// Gets or sets the options for this command. /// public Optional> Options { get; set; } + + /// + /// Whether the command is enabled by default when the app is added to a guild. Default is + /// + public Optional DefaultPermission { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs index 2cc0faf4f..3979e8835 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs @@ -18,7 +18,7 @@ namespace Discord /// /// The id of the interaction. /// - ulong Id { get; } + new ulong Id { get; } /// /// The type of this . diff --git a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs index 7160be976..eca4fdff6 100644 --- a/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs @@ -11,7 +11,7 @@ namespace Discord /// /// /// After receiving an interaction, you must respond to acknowledge it. You can choose to respond with a message immediately using - /// or you can choose to send a deferred response with . If choosing a deferred response, the user will see a loading state for the interaction, + /// or you can choose to send a deferred response with . If choosing a deferred response, the user will see a loading state for the interaction, /// and you'll have up to 15 minutes to edit the original deferred response using Edit Original Interaction Response. /// You can read more about Response types Here /// @@ -22,13 +22,13 @@ namespace Discord /// Pong = 1, - [Obsolete("This response type has been depricated by discord. Either use ChannelMessageWithSource or ACKWithSource", true)] + [Obsolete("This response type has been depricated by discord. Either use ChannelMessageWithSource or DeferredChannelMessageWithSource", true)] /// /// ACK a command without sending a message, eating the user's input. /// Acknowledge = 2, - [Obsolete("This response type has been depricated by discord. Either use ChannelMessageWithSource or ACKWithSource", true)] + [Obsolete("This response type has been depricated by discord. Either use ChannelMessageWithSource or DeferredChannelMessageWithSource", true)] /// /// Respond with a message, showing the user's input. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs index df9f39809..7f4a3a62d 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs @@ -26,5 +26,10 @@ namespace Discord /// Gets or sets the options for this command. /// public Optional> Options { get; set; } + + /// + /// Whether the command is enabled by default when the app is added to a guild. Default is + /// + public Optional DefaultPermission { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissions.cs new file mode 100644 index 000000000..86dbd5790 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Permissions/ApplicationCommandPermissions.cs @@ -0,0 +1,62 @@ +namespace Discord +{ + /// + /// Application command permissions allow you to enable or disable commands for specific users or roles within a guild. + /// + public class ApplicationCommandPermission + { + /// + /// The id of the role or user. + /// + public ulong Id { get; } + + /// + /// The target of this permission. + /// + public PermissionTarget Type { get; } + + /// + /// to allow, otherwise . + /// + public bool Value { get; } + + internal ApplicationCommandPermission() { } + + /// + /// Creates a new . + /// + /// The id you want to target this permission value for. + /// The type of the targetId parameter. + /// The value of this permission. + public ApplicationCommandPermission(ulong targetId, PermissionTarget targetType, bool allow) + { + this.Id = targetId; + this.Type = targetType; + this.Value = allow; + } + + /// + /// Creates a new targeting . + /// + /// The user you want to target this permission value for. + /// The value of this permission. + public ApplicationCommandPermission(IUser target, bool allow) + { + this.Id = target.Id; + this.Value = allow; + this.Type = PermissionTarget.User; + } + + /// + /// Creates a new targeting . + /// + /// The role you want to target this permission value for. + /// The value of this permission. + public ApplicationCommandPermission(IRole target, bool allow) + { + this.Id = target.Id; + this.Value = allow; + this.Type = PermissionTarget.Role; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildApplicationCommandPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildApplicationCommandPermissions.cs new file mode 100644 index 000000000..c91bfd59e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Permissions/GuildApplicationCommandPermissions.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Returned when fetching the permissions for a command in a guild. + /// + public class GuildApplicationCommandPermissions + { + /// + /// The id of the command. + /// + public ulong Id { get; } + + /// + /// The id of the application the command belongs to. + /// + public ulong ApplicationId { get; } + + /// + /// The id of the guild. + /// + public ulong GuildId { get; } + + /// + /// The permissions for the command in the guild. + /// + public IReadOnlyCollection Permissions { get; } + + internal GuildApplicationCommandPermissions(ulong id, ulong appId, ulong guildId, List permissions) + { + this.Id = id; + this.ApplicationId = appId; + this.GuildId = guildId; + this.Permissions = permissions.ToReadOnlyCollection(); + } + } +} diff --git a/src/Discord.Net.Core/Net/ApplicationCommandException.cs b/src/Discord.Net.Core/Net/ApplicationCommandException.cs new file mode 100644 index 000000000..7e94293f4 --- /dev/null +++ b/src/Discord.Net.Core/Net/ApplicationCommandException.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Net +{ + public class ApplicationCommandException : Exception + { + /// + /// Gets the JSON error code returned by Discord. + /// + /// + /// A + /// JSON error code + /// from Discord, or null if none. + /// + public int? DiscordCode { get; } + + /// + /// Gets the reason of the exception. + /// + public string Reason { get; } + + /// + /// Gets the request object used to send the request. + /// + public IRequest Request { get; } + + /// + /// The error object returned from discord. + /// + /// + /// Note: This object can be null if discord didn't provide it. + /// + public object Error { get; } + + /// + /// The request json used to create the application command. This is useful for checking your commands for any format errors. + /// + public string RequestJson { get; } + + /// + /// The underlying that caused this exception to be thrown. + /// + public HttpException InnerHttpException { get; } + /// + /// Initializes a new instance of the class. + /// + /// The request that was sent prior to the exception. + /// + /// + /// The Discord status code returned. + /// The reason behind the exception. + /// + public ApplicationCommandException(string requestJson, HttpException httpError) + : base("The application command failed to be created!", httpError) + { + Request = httpError.Request; + DiscordCode = httpError.DiscordCode; + Reason = httpError.Reason; + Error = httpError.Error; + RequestJson = requestJson; + InnerHttpException = httpError; + } + } +} diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index ff9cf91f2..568562b61 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -34,6 +34,13 @@ namespace Discord.Net /// Gets the request object used to send the request. /// public IRequest Request { get; } + /// + /// The error object returned from discord. + /// + /// + /// Note: This object can be null if discord didn't provide it. + /// + public object Error { get; } /// /// Initializes a new instance of the class. @@ -42,13 +49,14 @@ namespace Discord.Net /// The request that was sent prior to the exception. /// The Discord status code returned. /// The reason behind the exception. - public HttpException(HttpStatusCode httpCode, IRequest request, int? discordCode = null, string reason = null) + public HttpException(HttpStatusCode httpCode, IRequest request, int? discordCode = null, string reason = null, object errors = null) : base(CreateMessage(httpCode, discordCode, reason)) { HttpCode = httpCode; Request = request; DiscordCode = discordCode; Reason = reason; + Error = errors; } private static string CreateMessage(HttpStatusCode httpCode, int? discordCode = null, string reason = null) diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandPermissions.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandPermissions.cs new file mode 100644 index 000000000..105913adc --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandPermissions.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class ApplicationCommandPermissions + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("type")] + public PermissionTarget Type { get; set; } + + [JsonProperty("permission")] + public bool Permission { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/GuildApplicationCommandPermissions.cs b/src/Discord.Net.Rest/API/Common/GuildApplicationCommandPermissions.cs new file mode 100644 index 000000000..f64c14fff --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GuildApplicationCommandPermissions.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + public class GuildApplicationCommandPermission + { + [JsonProperty("id")] + public ulong Id { get; } + + [JsonProperty("application_id")] + public ulong ApplicationId { get; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; } + + [JsonProperty("permissions")] + public API.ApplicationCommandPermissions[] Permissions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissions.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissions.cs new file mode 100644 index 000000000..0d1792c94 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildApplicationCommandPermissions.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ModifyGuildApplicationCommandPermissions + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("permissions")] + public ApplicationCommandPermission[] Permissions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs index 4012debd7..04817f90c 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs @@ -10,7 +10,7 @@ namespace Discord.API.Rest internal class ModifyInteractionResponseParams { [JsonProperty("content")] - public string Content { get; set; } + public Optional Content { get; set; } [JsonProperty("embeds")] public Optional Embeds { get; set; } diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 4666d645d..f998b72f4 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -211,6 +211,7 @@ namespace Discord.Rest return response.Select(x => RestGlobalCommand.Create(client, x)).ToArray(); } + public static async Task> GetGuildApplicationCommands(BaseDiscordClient client, ulong guildId, RequestOptions options) { var response = await client.ApiClient.GetGuildApplicationCommandAsync(guildId, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index af33360f4..171a0eaf0 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -804,7 +804,21 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); - return await SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/commands", command, new BucketIds(), options: options).ConfigureAwait(false); + try + { + return await SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/commands", command, new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException x) + { + if (x.HttpCode == HttpStatusCode.BadRequest) + { + var json = (x.Request as JsonRestRequest).Json; + throw new ApplicationCommandException(json, x); + } + + // Re-throw the http exception + throw; + } } public async Task ModifyGlobalApplicationCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null) { @@ -823,7 +837,21 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); - return await SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options).ConfigureAwait(false); + try + { + return await SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options).ConfigureAwait(false); + } + catch (HttpException x) + { + if (x.HttpCode == HttpStatusCode.BadRequest) + { + var json = (x.Request as JsonRestRequest).Json; + throw new ApplicationCommandException(json, x); + } + + // Re-throw the http exception + throw; + } } public async Task DeleteGlobalApplicationCommandAsync(ulong commandId, RequestOptions options = null) { @@ -852,7 +880,21 @@ namespace Discord.API var bucket = new BucketIds(guildId: guildId); - return await SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, bucket, options: options).ConfigureAwait(false); + try + { + return await SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, bucket, options: options).ConfigureAwait(false); + } + catch (HttpException x) + { + if (x.HttpCode == HttpStatusCode.BadRequest) + { + var json = (x.Request as JsonRestRequest).Json; + throw new ApplicationCommandException(json, x); + } + + // Re-throw the http exception + throw; + } } public async Task ModifyGuildApplicationCommandAsync(ModifyApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null) { @@ -873,7 +915,21 @@ namespace Discord.API var bucket = new BucketIds(guildId: guildId); - return await SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, bucket, options: options).ConfigureAwait(false); + try + { + return await SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, bucket, options: options).ConfigureAwait(false); + } + catch (HttpException x) + { + if (x.HttpCode == HttpStatusCode.BadRequest) + { + var json = (x.Request as JsonRestRequest).Json; + throw new ApplicationCommandException(json, x); + } + + // Re-throw the http exception + throw; + } } public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) { @@ -894,6 +950,14 @@ namespace Discord.API await SendJsonAsync("POST", () => $"interactions/{interactionId}/{interactionToken}/callback", response, new BucketIds(), options: options); } + public async Task GetInteractionResponse(string interactionToken, RequestOptions options = null) + { + Preconditions.NotNullOrEmpty(interactionToken, nameof(interactionToken)); + + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", $"webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original").ConfigureAwait(false); + } public async Task ModifyInteractionResponse(ModifyInteractionResponseParams args, string interactionToken, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); @@ -920,13 +984,14 @@ namespace Discord.API return await SendJsonAsync("POST", () => $"webhooks/{CurrentUserId}/{token}?wait=true", args, new BucketIds(), options: options).ConfigureAwait(false); } - public async Task ModifyInteractionFollowupMessage(CreateWebhookMessageParams args, ulong id, string token, RequestOptions options = null) + public async Task ModifyInteractionFollowupMessage(ModifyInteractionResponseParams args, ulong id, string token, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(id, 0, nameof(id)); - if (args.Content?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + if(args.Content.IsSpecified) + if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); options = RequestOptions.CreateOrClone(options); @@ -942,6 +1007,56 @@ namespace Discord.API await SendAsync("DELETE", () => $"webhooks/{CurrentUserId}/{token}/messages/{id}", new BucketIds(), options: options).ConfigureAwait(false); } + // Application Command permissions + public async Task GetGuildApplicationCommandPermissions(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"/applications/{this.CurrentUserId}/guilds/{guildId}/commands/permissions", new BucketIds(), options: options).ConfigureAwait(false); + } + + public async Task GetGuildApplicationCommandPermission(ulong guildId, ulong commandId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(commandId, 0, nameof(commandId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"/applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}/permissions", new BucketIds(), options: options).ConfigureAwait(false); + } + + public async Task ModifyApplicationCommandPermissions(ApplicationCommandPermissions[] permissions, ulong guildId, ulong commandId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(commandId, 0, nameof(commandId)); + + options = RequestOptions.CreateOrClone(options); + + await SendJsonAsync("PUT", () => $"/applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}/permissions", permissions, new BucketIds(), options: options).ConfigureAwait(false); + } + + public async Task BatchModifyApplicationCommandPermissions(ModifyGuildApplicationCommandPermissions[] permissions, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(permissions, nameof(permissions)); + + options = RequestOptions.CreateOrClone(options); + + await SendJsonAsync("PUT", () => $"/applications/{this.CurrentUserId}/guilds/{guildId}/commands/premissions", permissions, new BucketIds(), options: options).ConfigureAwait(false); + } + + public async Task BulkOverrideGuildApplicationCommand(API.ApplicationCommand[] commands, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(commands, nameof(commands)); + + options = RequestOptions.CreateOrClone(options); + + await SendJsonAsync("PUT", () => $"/applications/{this.CurrentUserId}/guilds/{guildId}/commands", commands, new BucketIds(), options: options).ConfigureAwait(false); + } + //Guilds public async Task GetGuildAsync(ulong guildId, bool withCounts, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 46d92f815..c5a8347c7 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -10,12 +10,25 @@ namespace Discord.Rest { internal static class InteractionHelper { - internal static async Task SendFollowupAsync(BaseDiscordClient client, API.Rest.CreateWebhookMessageParams args, + internal static async Task SendInteractionResponse(BaseDiscordClient client, IMessageChannel channel, InteractionResponse response, + ulong interactionId, string interactionToken, RequestOptions options = null) + { + await client.ApiClient.CreateInteractionResponse(response, interactionId, interactionToken, options).ConfigureAwait(false); + + // get the original message + var msg = await client.ApiClient.GetInteractionResponse(interactionToken).ConfigureAwait(false); + + var entity = RestInteractionMessage.Create(client, msg, interactionToken, channel); + + return entity; + } + + internal static async Task SendFollowupAsync(BaseDiscordClient client, API.Rest.CreateWebhookMessageParams args, string token, IMessageChannel channel, RequestOptions options = null) { var model = await client.ApiClient.CreateInteractionFollowupMessage(args, token, options).ConfigureAwait(false); - var entity = RestUserMessage.Create(client, channel, client.CurrentUser, model); + RestFollowupMessage entity = RestFollowupMessage.Create(client, model, token, channel); return entity; } @@ -44,7 +57,10 @@ namespace Discord.Rest Description = args.Description, Options = args.Options.IsSpecified ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified + : Optional.Unspecified, + DefaultPermission = args.DefaultPermission.IsSpecified + ? args.DefaultPermission.Value + : Optional.Unspecified }; var cmd = await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false); @@ -68,7 +84,10 @@ namespace Discord.Rest Description = args.Description, Options = args.Options.IsSpecified ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified + : Optional.Unspecified, + DefaultPermission = args.DefaultPermission.IsSpecified + ? args.DefaultPermission.Value + : Optional.Unspecified }; var msg = await client.ApiClient.ModifyGlobalApplicationCommandAsync(model, command.Id, options).ConfigureAwait(false); @@ -119,7 +138,10 @@ namespace Discord.Rest Description = args.Description, Options = args.Options.IsSpecified ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified + : Optional.Unspecified, + DefaultPermission = args.DefaultPermission.IsSpecified + ? args.DefaultPermission.Value + : Optional.Unspecified }; var cmd = await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false); @@ -143,7 +165,10 @@ namespace Discord.Rest Description = args.Description, Options = args.Options.IsSpecified ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified + : Optional.Unspecified, + DefaultPermission = args.DefaultPermission.IsSpecified + ? args.DefaultPermission.Value + : Optional.Unspecified }; var msg = await client.ApiClient.ModifyGuildApplicationCommandAsync(model, command.GuildId, command.Id, options).ConfigureAwait(false); @@ -158,5 +183,60 @@ namespace Discord.Rest await client.ApiClient.DeleteGuildApplicationCommandAsync(command.GuildId, command.Id, options).ConfigureAwait(false); } + + internal static async Task ModifyFollowupMessage(BaseDiscordClient client, RestFollowupMessage message, Action func, + RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(message.Content); + bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : message.Embeds.Any(); + if (!hasText && !hasEmbed) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiArgs = new API.Rest.ModifyInteractionResponseParams + { + Content = args.Content, + Embeds = args.Embed.IsSpecified ? new API.Embed[] { args.Embed.Value.ToModel() } : Optional.Create() + }; + + return await client.ApiClient.ModifyInteractionFollowupMessage(apiArgs, message.Id, message.Token, options).ConfigureAwait(false); + } + + internal static async Task DeleteFollowupMessage(BaseDiscordClient client, RestFollowupMessage message, RequestOptions options = null) + => await client.ApiClient.DeleteInteractionFollowupMessage(message.Id, message.Token, options); + + internal static async Task ModifyInteractionResponse(BaseDiscordClient client, RestInteractionMessage message, Action func, + RequestOptions options = null) + { + var args = new MessageProperties(); + func(args); + + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(message.Content); + bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : message.Embeds.Any(); + if (!hasText && !hasEmbed) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + var apiArgs = new API.Rest.ModifyInteractionResponseParams + { + Content = args.Content, + Embeds = args.Embed.IsSpecified ? new API.Embed[] { args.Embed.Value.ToModel() } : Optional.Create() + }; + + return await client.ApiClient.ModifyInteractionFollowupMessage(apiArgs, message.Id, message.Token, options).ConfigureAwait(false); + } + + internal static async Task DeletedInteractionResponse(BaseDiscordClient client, RestInteractionMessage message, RequestOptions options = null) + => await client.ApiClient.DeleteInteractionFollowupMessage(message.Id, message.Token, options); + + // Guild permissions + internal static async Task> GetCommandGuildPermissions(BaseDiscordClient client, + RestGuildCommand command) + { + // TODO + return null; + } + } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs index 87e7bc886..90e998977 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs @@ -45,5 +45,8 @@ namespace Discord.Rest /// public async Task ModifyAsync(Action func, RequestOptions options = null) => await InteractionHelper.ModifyGuildCommand(Discord, this, func, options).ConfigureAwait(false); + + public async Task> GetCommandPermissions() + => await InteractionHelper.GetCommandGuildPermissions(Discord, this); } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestFollowupMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestFollowupMessage.cs new file mode 100644 index 000000000..a7c65b3a1 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestFollowupMessage.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Message; + + +namespace Discord.Rest +{ + /// + /// Represents a REST-based follow up message sent by a bot responding to a slash command. + /// + public class RestFollowupMessage : RestUserMessage + { + // Token used to delete/modify this followup message + internal string Token { get; } + + internal RestFollowupMessage(BaseDiscordClient discord, ulong id, IUser author, string token, IMessageChannel channel) + : base(discord, id, channel, author, MessageSource.Bot) + { + this.Token = token; + } + + internal static RestFollowupMessage Create(BaseDiscordClient discord, Model model, string token, IMessageChannel channel) + { + var entity = new RestFollowupMessage(discord, model.Id, model.Author.IsSpecified ? RestUser.Create(discord, model.Author.Value) : discord.CurrentUser, token, channel); + entity.Update(model); + return entity; + } + + internal new void Update(Model model) + { + base.Update(model); + } + + /// + /// Deletes this object and all of it's childern. + /// + /// A task that represents the asynchronous delete operation. + public Task DeleteAsync() + => InteractionHelper.DeleteFollowupMessage(Discord, this); + + /// + /// Modifies this interaction followup message. + /// + /// + /// This method modifies this message with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// + /// The following example replaces the content of the message with Hello World!. + /// + /// await msg.ModifyAsync(x => x.Content = "Hello World!"); + /// + /// + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// The token used to modify/delete this message expired. + /// /// Somthing went wrong during the request. + public new async Task ModifyAsync(Action func, RequestOptions options = null) + { + try + { + var model = await InteractionHelper.ModifyFollowupMessage(Discord, this, func, options).ConfigureAwait(false); + this.Update(model); + } + catch (Discord.Net.HttpException x) + { + if(x.HttpCode == System.Net.HttpStatusCode.NotFound) + { + throw new InvalidOperationException("The token of this message has expired!", x); + } + + throw; + } + } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs new file mode 100644 index 000000000..6733966ad --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/RestInteractionMessage.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord.Rest +{ + /// + /// Represents the initial REST-based response to a slash command. + /// + public class RestInteractionMessage : RestUserMessage + { + // Token used to delete/modify this followup message + internal string Token { get; } + + internal RestInteractionMessage(BaseDiscordClient discord, ulong id, IUser author, string token, IMessageChannel channel) + : base(discord, id, channel, author, MessageSource.Bot) + { + this.Token = token; + } + + internal static RestInteractionMessage Create(BaseDiscordClient discord, Model model, string token, IMessageChannel channel) + { + var entity = new RestInteractionMessage(discord, model.Id, model.Author.IsSpecified ? RestUser.Create(discord, model.Author.Value) : discord.CurrentUser, token, channel); + entity.Update(model); + return entity; + } + + internal new void Update(Model model) + { + base.Update(model); + } + + /// + /// Deletes this object and all of it's childern. + /// + /// A task that represents the asynchronous delete operation. + public Task DeleteAsync() + => InteractionHelper.DeletedInteractionResponse(Discord, this); + + /// + /// Modifies this interaction response + /// + /// + /// This method modifies this message with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// + /// The following example replaces the content of the message with Hello World!. + /// + /// await msg.ModifyAsync(x => x.Content = "Hello World!"); + /// + /// + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// The token used to modify/delete this message expired. + /// /// Somthing went wrong during the request. + public new async Task ModifyAsync(Action func, RequestOptions options = null) + { + try + { + var model = await InteractionHelper.ModifyInteractionResponse(Discord, this, func, options).ConfigureAwait(false); + this.Update(model); + } + catch (Discord.Net.HttpException x) + { + if (x.HttpCode == System.Net.HttpStatusCode.NotFound) + { + throw new InvalidOperationException("The token of this message has expired!", x); + } + + throw; + } + } + } +} diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 3fb45e55d..d986f5c37 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -99,6 +99,7 @@ namespace Discord.Net.Queue default: int? code = null; string reason = null; + object errors = null; if (response.Stream != null) { try @@ -109,11 +110,12 @@ namespace Discord.Net.Queue var json = JToken.Load(jsonReader); try { code = json.Value("code"); } catch { }; try { reason = json.Value("message"); } catch { }; + try { errors = json.Value("errors"); } catch { }; } } catch { } } - throw new HttpException(response.StatusCode, request, code, reason); + throw new HttpException(response.StatusCode, request, code, reason, errors); } } else diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 171094ade..e234240ef 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -111,7 +111,7 @@ namespace Discord.WebSocket /// /// /// Discord interactions will not appear in chat until the client responds to them. With this option set to - /// , the client will automatically acknowledge the interaction with . + /// , the client will automatically acknowledge the interaction with . /// See the docs on /// responding to interactions for more info. /// diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index 6e7efaf66..92bc679d8 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -28,8 +28,7 @@ namespace Discord.WebSocket /// /// The who triggered this interaction. /// - public SocketGuildUser User - => Guild.GetUser(UserId); + public SocketGuildUser User { get; private set; } /// /// The type of this interaction. @@ -87,6 +86,9 @@ namespace Discord.WebSocket this.Version = model.Version; this.UserId = model.Member.User.Id; this.Type = model.Type; + + if (this.User == null) + this.User = SocketGuildUser.Create(this.Guild, Discord.State, model.Member); // Change from getter. } private bool CheckToken() {