diff --git a/docs/guides/interactions/application-commands/01-getting-started.md b/docs/guides/interactions/application-commands/01-getting-started.md index 70d0bd6f7..fc8c8fe30 100644 --- a/docs/guides/interactions/application-commands/01-getting-started.md +++ b/docs/guides/interactions/application-commands/01-getting-started.md @@ -1,3 +1,9 @@ +--- +uid: Guides.SlashCommands.Intro +title: Introduction to slash commands +--- + + # Getting started with application commands. Welcome! This guide will show you how to use application commands. @@ -22,4 +28,5 @@ Head over to your discord applications OAuth2 screen and make sure to select the From there you can then use the link to add your bot to a server. -**Note**: In order for users in your guild to use your slash commands, they need to have the "Use Slash Command" permission on the guild. +> [!NOTE] +> In order for users in your guild to use your slash commands, they need to have the "Use Slash Command" permission on the guild. diff --git a/docs/guides/interactions/application-commands/context-menu-commands/creating-context-menu-commands.md b/docs/guides/interactions/application-commands/context-menu-commands/creating-context-menu-commands.md index d497ca1e6..02a9cde14 100644 --- a/docs/guides/interactions/application-commands/context-menu-commands/creating-context-menu-commands.md +++ b/docs/guides/interactions/application-commands/context-menu-commands/creating-context-menu-commands.md @@ -1,3 +1,8 @@ +--- +uid: Guides.ContextCommands.Creating +title: Creating Context Commands +--- + # Creating context menu commands. There are two kinds of Context Menu Commands: User Commands and Message Commands. @@ -96,4 +101,5 @@ public async Task Client_Ready() ``` -**Note**: Application commands only need to be created once. They do _not_ have to be 'created' on every startup or connection. The example simple shows creating them in the ready event as it's simpler than creating normal bot commands to register application commands. +> [!NOTE] +> Application commands only need to be created once. They do _not_ have to be 'created' on every startup or connection. The example simple shows creating them in the ready event as it's simpler than creating normal bot commands to register application commands. diff --git a/docs/guides/interactions/application-commands/context-menu-commands/receiving-context-menu-command-events.md b/docs/guides/interactions/application-commands/context-menu-commands/receiving-context-menu-command-events.md index 52bc303e7..d4e973d04 100644 --- a/docs/guides/interactions/application-commands/context-menu-commands/receiving-context-menu-command-events.md +++ b/docs/guides/interactions/application-commands/context-menu-commands/receiving-context-menu-command-events.md @@ -1,40 +1,33 @@ +--- +uid: Guides.ContextCommands.Reveiving +title: Receiving Context Commands +--- + # Receiving Context Menu events -User commands and Message commands have their own unique objects returned. Different from Slash commands. To get the appropriate object returned, you can use a similar method to the slash commands. +User commands and Message commands have their own unique event just like the other interaction types. For user commands the event is `UserCommandExecuted` and for message commands the event is `MessageCommandExecuted`. ```cs -client.InteractionCreated += InteractionCreatedHandler; +// For message commands +client.MessageCommandExecuted += MessageCommandHandler; + +// For user commands +client.UserCommandExecuted += UserCommandHandler; ... -public async Task InteractionCreatedHandler(SocketInteraction arg) +public async Task MessageCommandHandler(SocketMessageCommand arg) { - if ( arg.Type == InteractionType.ApplicationCommand) - Task.Run(() => ApplicationCommandHandler(arg)); + Console.Writeline("Message command received!"); } -public async Task ApplicationCommandHandler(SocketInteraction arg) +public async Task UserCommandHandler(SocketUserCommand arg) { - switch (arg) - { - case SocketSlashCommand slashCommand: - Console.Writeline("Slash command received!"); - break; - case SocketUserCommand userCommand: - Console.Writeline("User command received!") - // userCommand.User = User who ran command. - // userCommand.Data.Member = User who was clicked. - break; - case SocketMessageCommand messageCommand: - Console.Writeline("Message command received!") - // messageCommand.User = User who ran command. - // messageCommand.Data.Message = Message that was clicked. - break; - } + Console.Writeline("User command received!"); } ``` -User commands return a SocketUser object, showing the user that was clicked to run the command. -Message commands return a SocketMessage object, showing the message that was clicked to run the command. +User commands contain a SocketUser object called `Member` in their data class, showing the user that was clicked to run the command. +Message commands contain a SocketMessage object called `Message` in their data class, showing the message that was clicked to run the command. Both return the user who ran the command, the guild (if any), channel, etc. \ No newline at end of file diff --git a/docs/guides/interactions/application-commands/slash-commands/03-responding-to-slash-commands.md b/docs/guides/interactions/application-commands/slash-commands/03-responding-to-slash-commands.md index cd6ad5217..3dbc579fe 100644 --- a/docs/guides/interactions/application-commands/slash-commands/03-responding-to-slash-commands.md +++ b/docs/guides/interactions/application-commands/slash-commands/03-responding-to-slash-commands.md @@ -5,40 +5,25 @@ title: Receiving and Responding to Slash Commands # Responding to interactions. -Interactions are the base thing sent over by Discord. Slash commands are one of the interaction types. In order to receive a slash command we have to listen to the `InteractionCreated` event. Let's add this to our code. +Interactions are the base thing sent over by Discord. Slash commands are one of the interaction types. We can listen to the `SlashCommandExecuted` event to respond to them. Lets add this to our code: ```cs -client.InteractionCreated += Client_InteractionCreated; +client.SlashCommandExecuted += SlashCommandHandler; ... -private async Task Client_InteractionCreated(SocketInteraction arg) +private async Task SlashCommandHandler(SocketSlashCommand command) { } ``` -Now that we have the interaction event, let's talk about the `SocketInteraction` argument. The interaction can be cast to either a `SocketSlashCommand` or a `SocketMessageComponent`. In our case, we're trying to use slash commands so let's cast it to a `SocketSlashCommand`. - -```cs -private async Task Client_InteractionCreated(SocketInteraction arg) -{ - if(arg is SocketSlashCommand command) - { - // we now have an instance of a SocketSlashCommand named command. - } -} -``` - With every type of interaction there is a `Data` field. This is where the relevant information lives about our command that was executed. In our case, `Data` is a `SocketSlashCommandData` instance. In the data class, we can access the name of the command triggered as well as the options if there were any. For this example, we're just going to respond with the name of the command executed. ```cs -private async Task Client_InteractionCreated(SocketInteraction arg) +private async Task SlashCommandHandler(SocketSlashCommand command) { - if(arg is SocketSlashCommand command) - { - await command.RespondAsync($"You executed {command.Data.Name}"); - } + await command.RespondAsync($"You executed {command.Data.Name}"); } ``` @@ -48,8 +33,6 @@ Let's try this out! ![slash command result](images/slashcommand2.png) -Let's go over the response types quickly, as you would only change them for style points :P - > [!NOTE] > After receiving an interaction, you must respond to acknowledge it. You can choose to respond with a message immediately using `RespondAsync()` or you can choose to send a deferred response with `DeferAsync()`. > 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 `ModifyOriginalResponseAsync()`. You can read more about response types [here](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) diff --git a/docs/guides/interactions/application-commands/slash-commands/04-parameters.md b/docs/guides/interactions/application-commands/slash-commands/04-parameters.md index cc61796c8..6afd83729 100644 --- a/docs/guides/interactions/application-commands/slash-commands/04-parameters.md +++ b/docs/guides/interactions/application-commands/slash-commands/04-parameters.md @@ -46,7 +46,7 @@ public async Task Client_Ready() var guildCommand = new SlashCommandBuilder() .WithName("list-roles") .WithDescription("Lists all roles of a user.") - .AddOption("user", ApplicationCommandOptionType.User, "The users whos roles you want to be listed", required: true); + .AddOption("user", ApplicationCommandOptionType.User, "The users whos roles you want to be listed", isRequired: true); try { @@ -66,17 +66,14 @@ public async Task Client_Ready() That seems to be working, now Let's handle the interaction. ```cs -private async Task Client_InteractionCreated(SocketInteraction arg) +private async Task SlashCommandHandler(SocketSlashCommand command) { - if(arg is SocketSlashCommand command) + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) { - // Let's add a switch statement for the command name so we can handle multiple commands in one event. - switch(command.Data.Name) - { - case "list-roles": - await HandleListRoleCommand(command); - break; - } + case "list-roles": + await HandleListRoleCommand(command); + break; } } diff --git a/docs/guides/interactions/application-commands/slash-commands/05-responding-ephemerally.md b/docs/guides/interactions/application-commands/slash-commands/05-responding-ephemerally.md index fa5ac449b..641103df5 100644 --- a/docs/guides/interactions/application-commands/slash-commands/05-responding-ephemerally.md +++ b/docs/guides/interactions/application-commands/slash-commands/05-responding-ephemerally.md @@ -7,7 +7,8 @@ title: Ephemeral Responses What is an ephemeral response? Basically, only the user who executed the command can see the result of it, this is pretty simple to implement. -**Note**: You don't have to run arg.DeferAsync() to capture the interaction, you can use arg.RespondAsync() with a message to capture it, this also follows the ephemeral rule. +> [!NOTE] +> You don't have to run arg.DeferAsync() to capture the interaction, you can use arg.RespondAsync() with a message to capture it, this also follows the ephemeral rule. When responding with either `FollowupAsync` or `RespondAsync` you can pass in an `ephemeral` property. When setting it to true it will respond ephemerally, false and it will respond non-ephemerally. diff --git a/docs/guides/interactions/application-commands/slash-commands/06-subcommands.md b/docs/guides/interactions/application-commands/slash-commands/06-subcommands.md index ac8c7313d..83d7b283c 100644 --- a/docs/guides/interactions/application-commands/slash-commands/06-subcommands.md +++ b/docs/guides/interactions/application-commands/slash-commands/06-subcommands.md @@ -86,7 +86,7 @@ public async Task Client_Ready() .WithName("set") .WithDescription("Sets the field A") .WithType(ApplicationCommandOptionType.SubCommand) - .AddOption("value", ApplicationCommandOptionType.String, "the value to set the field ", required: true) + .AddOption("value", ApplicationCommandOptionType.String, "the value to set the field", isRequired: true) ).AddOption(new SlashCommandOptionBuilder() .WithName("get") .WithDescription("Gets the value of field A.") @@ -100,7 +100,7 @@ public async Task Client_Ready() .WithName("set") .WithDescription("Sets the field B") .WithType(ApplicationCommandOptionType.SubCommand) - .AddOption("value", ApplicationCommandOptionType.Integer, "the value to set the fie to.", required: true) + .AddOption("value", ApplicationCommandOptionType.Integer, "the value to set the fie to.", isRequired: true) ).AddOption(new SlashCommandOptionBuilder() .WithName("get") .WithDescription("Gets the value of field B.") @@ -114,7 +114,7 @@ public async Task Client_Ready() .WithName("set") .WithDescription("Sets the field C") .WithType(ApplicationCommandOptionType.SubCommand) - .AddOption("value", ApplicationCommandOptionType.Boolean, "the value to set the fie to.", required: true) + .AddOption("value", ApplicationCommandOptionType.Boolean, "the value to set the fie to.", isRequired: true) ).AddOption(new SlashCommandOptionBuilder() .WithName("get") .WithDescription("Gets the value of field C.") @@ -140,20 +140,17 @@ All that code generates a command that looks like this: Now that we have our command made, we need to handle the multiple options with this command. So lets add this into our handler: ```cs -private async Task Client_InteractionCreated(SocketInteraction arg) +private async Task SlashCommandHandler(SocketSlashCommand command) { - if(arg is SocketSlashCommand command) + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) { - // Let's add a switch statement for the command name so we can handle multiple commands in one event. - switch(command.Data.Name) - { - case "list-roles": - await HandleListRoleCommand(command); - break; - case "settings": - await HandleSettingsCommand(command); - break; - } + case "list-roles": + await HandleListRoleCommand(command); + break; + case "settings": + await HandleSettingsCommand(command); + break; } } diff --git a/docs/guides/interactions/application-commands/slash-commands/07-choice-slash-command.md b/docs/guides/interactions/application-commands/slash-commands/07-choice-slash-command.md index 79de5ffbc..3951e1141 100644 --- a/docs/guides/interactions/application-commands/slash-commands/07-choice-slash-command.md +++ b/docs/guides/interactions/application-commands/slash-commands/07-choice-slash-command.md @@ -40,8 +40,8 @@ private async Task Client_Ready() } } ``` - -> **Note:** Your `ApplicationCommandOptionType` specifies which type your choices are, you need to use `ApplicationCommandOptionType.Integer` if choices whos value are numbers and `ApplicationCommandOptionType.String` for string values. +> [!NOTE] +> Your `ApplicationCommandOptionType` specifies which type your choices are, you need to use `ApplicationCommandOptionType.Integer` for choices whos values are whole numbers, `ApplicationCommandOptionType.Number` for choices whos values are doubles, and `ApplicationCommandOptionType.String` for string values. We have defined 5 choices for the user to pick from, each choice has a value assigned to it. The value can either be a string or an int. In our case we're going to use an int. This is what the command looks like: @@ -50,23 +50,20 @@ We have defined 5 choices for the user to pick from, each choice has a value ass Lets add our code for handling the interaction. ```cs -private async Task Client_InteractionCreated(SocketInteraction arg) +private async Task SlashCommandHandler(SocketSlashCommand command) { - if(arg is SocketSlashCommand command) + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) { - // Let's add a switch statement for the command name so we can handle multiple commands in one event. - switch(command.Data.Name) - { - case "list-roles": - await HandleListRoleCommand(command); - break; - case "settings": - await HandleSettingsCommand(HandleFeedbackCommand); - break; - case "feedback": - await HandleFeedbackCommand(command); - break; - } + case "list-roles": + await HandleListRoleCommand(command); + break; + case "settings": + await HandleSettingsCommand(command); + break; + case "feedback": + await HandleFeedbackCommand(command); + break; } } diff --git a/docs/guides/interactions/application-commands/slash-commands/08-bulk-overwrite-of-global-slash-commands.md b/docs/guides/interactions/application-commands/slash-commands/08-bulk-overwrite-of-global-slash-commands.md index e2511ab98..095eda14f 100644 --- a/docs/guides/interactions/application-commands/slash-commands/08-bulk-overwrite-of-global-slash-commands.md +++ b/docs/guides/interactions/application-commands/slash-commands/08-bulk-overwrite-of-global-slash-commands.md @@ -6,9 +6,11 @@ title: Slash Command Bulk Overwrites If you have too many global commands then you might want to consider using the bulk overwrite function. ```cs -public async Task Client_Ready() { +public async Task Client_Ready() +{ List applicationCommandProperties = new(); - try { + try + { // Simple help slash command. SlashCommandBuilder globalCommandHelp = new SlashCommandBuilder(); globalCommandHelp.WithName("help"); @@ -28,10 +30,11 @@ public async Task Client_Ready() { applicationCommandProperties.Add(globalCommandAddFamily.Build()); await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray()); - } catch (ApplicationCommandException exception) { + } + catch (ApplicationCommandException exception) + { var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); Console.WriteLine(json); } - Console.WriteLine("Client Ready: Finished"); } ``` diff --git a/docs/guides/interactions/message-components/01-getting-started.md b/docs/guides/interactions/message-components/01-getting-started.md index 2b1d3f5ff..cd5eadd0a 100644 --- a/docs/guides/interactions/message-components/01-getting-started.md +++ b/docs/guides/interactions/message-components/01-getting-started.md @@ -1,3 +1,8 @@ +--- +uid: Guides.MessageComponents.GettingStarted +title: Getting Started with Components +--- + # Message Components Message components are a framework for adding interactive elements to a message your app or bot sends. They're accessible, customizable, and easy to use. diff --git a/docs/guides/interactions/message-components/02-responding-to-buttons.md b/docs/guides/interactions/message-components/02-responding-to-buttons.md index c83f1b9d2..00d651f6b 100644 --- a/docs/guides/interactions/message-components/02-responding-to-buttons.md +++ b/docs/guides/interactions/message-components/02-responding-to-buttons.md @@ -1,30 +1,23 @@ +--- +uid: Guides.MessageComponents.Responding +title: Responding to Components +--- + # Responding to button clicks Responding to buttons is pretty simple, there are a couple ways of doing it and we can cover both. ### Method 1: Hooking the InteractionCreated Event -We can hook the `InteractionCreated` event since button clicks are a form of interactions: +We can hook the `ButtonExecuted` event for button type interactions: ```cs -client.IntreactionCreated += MyInteractionHandler; +client.ButtonExecuted += MyButtonHandler; ``` Now, lets write our handler. ```cs -public async Task MyInteractionHandler(SocketInteraction arg) -{ - // first we check the type of the interaction, this can be done with a switch statement - switch(arg) - { - case SocketMessageComponent component: - // we now have a variable defined as 'component' which contains our component data, lets pass it to a different handler. - - break; - } -} - public async Task MyButtonHandler(SocketMessageComponent component) { // We can now check for our custom id @@ -41,14 +34,4 @@ public async Task MyButtonHandler(SocketMessageComponent component) Running it and clicking the button: -![](Images/image2.png) - -### Method 2: Hooking the ButtonExecuted Event - -This method skips the first switch statement because the `ButtonExecuted` event is only fired when a button is clicked, meaning we dont have to check the type of the interaction. - -```cs -client.ButtonExecuted += MyButtonHandler; -``` - -The rest of the code is the same and produces the same result. +![](Images/image2.png) \ No newline at end of file diff --git a/docs/guides/interactions/message-components/03-buttons-in-depth.md b/docs/guides/interactions/message-components/03-buttons-in-depth.md index db3868501..f9fd67515 100644 --- a/docs/guides/interactions/message-components/03-buttons-in-depth.md +++ b/docs/guides/interactions/message-components/03-buttons-in-depth.md @@ -1,3 +1,8 @@ +--- +uid: Guides.MessageComponents.Buttons +title: Buttons in Depth +--- + # Buttons in depth There are many changes you can make to buttons, lets take a look at the parameters in the `WithButton` function" diff --git a/docs/guides/interactions/message-components/04-select-menus.md b/docs/guides/interactions/message-components/04-select-menus.md index 3f06aa389..5181ddf34 100644 --- a/docs/guides/interactions/message-components/04-select-menus.md +++ b/docs/guides/interactions/message-components/04-select-menus.md @@ -1,3 +1,8 @@ +--- +uid: Guides.MessageComponents.SelectMenus +title: Select Menus +--- + # Select menus Select menus allow users to select from a range of options, this can be quite useful with configuration commands etc. @@ -48,7 +53,7 @@ And opening the menu we see: ![](Images/image5.png) -Lets handle the selection of an option, as before we can hook the `InteractionCreated` event and check the type ourself but for this example im just going to use the `SelectMenuExecuted` event +Lets handle the selection of an option, We can hook the `SelectMenuExecuted` event to handle our select menu: ```cs client.SelectMenuExecuted += MyMenuHandler; diff --git a/docs/guides/interactions/message-components/05-advanced.md b/docs/guides/interactions/message-components/05-advanced.md index 638886ad3..49b3f31a6 100644 --- a/docs/guides/interactions/message-components/05-advanced.md +++ b/docs/guides/interactions/message-components/05-advanced.md @@ -1,3 +1,8 @@ +--- +uid: Guides.MessageComponents.Advanced +title: Advanced Concepts +--- + # Advanced Lets say you have some components on an ephemeral slash command, and you want to modify the message that the button is on. The issue with this is that ephemeral messages are not stored and can not be get via rest or other means. @@ -45,31 +50,38 @@ break; Now, let's listen to the select menu executed event and add a case for `select-1` ```cs -switch (arg.Data.CustomId) +client.SelectMenuExecuted += SelectMenuHandler; + +... + +public async Task SelectMenuHandler(SocketMessageComponent arg) { - case "select-1": - var value = arg.Data.Values.First(); - var menu = new SelectMenuBuilder() - { - CustomId = "select-1", - Placeholder = $"{(arg.Message.Components.First().Components.First() as SelectMenu).Options.FirstOrDefault(x => x.Value == value).Label}", - MaxValues = 1, - MinValues = 1, - Disabled = true - }; - - menu.AddOption("Meh", "1", "Its not gaming.") - .AddOption("Ish", "2", "Some would say that this is gaming.") - .AddOption("Moderate", "3", "It could pass as gaming") - .AddOption("Confirmed", "4", "We are gaming") - .AddOption("Excellent", "5", "It is renowned as gaming nation wide", new Emoji("🔥")); - - // We use UpdateAsync to update the message and its original content and components. - await arg.UpdateAsync(x => - { - x.Content = $"Thank you {arg.User.Mention} for rating us {value}/5 on the gaming scale"; - x.Components = new ComponentBuilder().WithSelectMenu(menu).Build(); - }); - break; + switch (arg.Data.CustomId) + { + case "select-1": + var value = arg.Data.Values.First(); + var menu = new SelectMenuBuilder() + { + CustomId = "select-1", + Placeholder = $"{(arg.Message.Components.First().Components.First() as SelectMenu).Options.FirstOrDefault(x => x.Value == value).Label}", + MaxValues = 1, + MinValues = 1, + Disabled = true + }; + + menu.AddOption("Meh", "1", "Its not gaming.") + .AddOption("Ish", "2", "Some would say that this is gaming.") + .AddOption("Moderate", "3", "It could pass as gaming") + .AddOption("Confirmed", "4", "We are gaming") + .AddOption("Excellent", "5", "It is renowned as gaming nation wide", new Emoji("🔥")); + + // We use UpdateAsync to update the message and its original content and components. + await arg.UpdateAsync(x => + { + x.Content = $"Thank you {arg.User.Mention} for rating us {value}/5 on the gaming scale"; + x.Components = new ComponentBuilder().WithSelectMenu(menu).Build(); + }); + break; + } } ``` diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 64ed1c8cc..10e91760b 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -35,10 +35,10 @@ topicUid: Guides.Commands.DI - name: Post-execution Handling topicUid: Guides.Commands.PostExecution -- name: Working with Interactions +- name: Working with Slash commands items: - name: Introduction - topicUid: Guides.Interactions.Intro + topicUid: Guides.SlashCommands.Intro - name: Creating slash commands topicUid: Guides.SlashCommands.Creating - name: Receiving and responding to slash commands @@ -51,8 +51,26 @@ topicUid: Guides.SlashCommands.SubCommand - name: Slash command choices topicUid: Guides.SlashCommands.Choices - - name: Slash ommands Bulk Overwrites + - name: Slash commands Bulk Overwrites topicUid: Guides.SlashCommands.BulkOverwrite +- name: Working with Context commands + items: + - name: Creating Context Commands + topicUid: Guides.ContextCommands.Creating + - name: Receiving Context Commands + topicUid: Guides.ContextCommands.Reveiving +- name: Working with Message Components + items: + - name: Getting started + topicUid: Guides.MessageComponents.GettingStarted + - name: Responding to Components + topicUid: Guides.MessageComponents.Responding + - name: Buttons in depth + topicUid: Guides.MessageComponents.Buttons + - name: Select menus + topicUid: Guides.MessageComponents.SelectMenus + - name: Advanced Concepts + topicUid: Guides.MessageComponents.Advanced - name: Emoji topicUid: Guides.Emoji - name: Voice diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index b170336dc..d6535a4f1 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -47,6 +47,14 @@ namespace Discord return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; } + public static string GetGuildUserAvatarUrl(ulong userId, ulong guildId, string avatarId, ushort size, ImageFormat format) + { + if (avatarId == null) + return null; + string extension = FormatToExtension(format, avatarId); + return $"{DiscordConfig.CDNUrl}guilds/{guildId}/users/{userId}/avatars/{avatarId}.{extension}?size={size}"; + } + /// /// Returns a user banner URL. /// @@ -131,15 +139,17 @@ namespace Discord /// /// The guild snowflake identifier. /// The banner image identifier. + /// The format to return. /// The size of the image to return in horizontal pixels. This can be any power of two between 16 and 2048 inclusive. /// /// A URL pointing to the guild's banner image. /// - public static string GetGuildBannerUrl(ulong guildId, string bannerId, ushort? size = null) + public static string GetGuildBannerUrl(ulong guildId, string bannerId, ImageFormat format, ushort? size = null) { - if (!string.IsNullOrEmpty(bannerId)) - return $"{DiscordConfig.CDNUrl}banners/{guildId}/{bannerId}.jpg" + (size.HasValue ? $"?size={size}" : string.Empty); - return null; + if (string.IsNullOrEmpty(bannerId)) + return null; + string extension = FormatToExtension(format, bannerId); + return $"{DiscordConfig.CDNUrl}banners/{guildId}/{bannerId}.{extension}" + (size.HasValue ? $"?size={size}" : string.Empty); } /// /// Returns an emoji URL. diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityProperties.cs b/src/Discord.Net.Core/Entities/Activities/ActivityProperties.cs index a7d13235f..587b21f03 100644 --- a/src/Discord.Net.Core/Entities/Activities/ActivityProperties.cs +++ b/src/Discord.Net.Core/Entities/Activities/ActivityProperties.cs @@ -33,6 +33,18 @@ namespace Discord /// /// Indicates that a user can play this song. /// - Play = 0b100000 + Play = 0b100000, + /// + /// Indicates that a user is playing an activity in a voice channel with friends. + /// + PARTY_PRIVACY_FRIENDS = 0b1000000, + /// + /// Indicates that a user is playing an activity in a voice channel. + /// + PARTY_PRIVACY_VOICE_CHANNEL = 0b10000000, + /// + /// Indicates that a user is playing an activity in a voice channel. + /// + EMBEDDED = 0b10000000 } } diff --git a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs index 85fc0b811..e60bd5031 100644 --- a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs @@ -24,7 +24,8 @@ namespace Discord /// The channel is a private temporary thread channel under a text channel. PrivateThread = 12, /// The channel is a stage voice channel. - Stage = 13 - + Stage = 13, + /// The channel is a guild directory used in hub servers. (Unreleased) + GuildDirectory = 14 } } diff --git a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs index a58112b28..93cda8d5b 100644 --- a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs @@ -50,6 +50,11 @@ namespace Discord /// This value may not be set if the role is an @everyone role. /// public Optional Hoist { get; set; } + + /// + /// Gets or sets the icon of the role. + /// + public Optional Icon { get; set; } /// /// Gets or sets whether or not this role can be mentioned. /// diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 58a797c5f..947ff8521 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -25,6 +25,13 @@ namespace Discord /// string Nickname { get; } /// + /// Gets the guild specific avatar for this users. + /// + /// + /// The users guild avatar hash if they have one; otherwise . + /// + string GuildAvatarId { get; } + /// /// Gets the guild-level permissions for this user. /// /// @@ -96,6 +103,20 @@ namespace Discord /// ChannelPermissions GetPermissions(IGuildChannel channel); + /// + /// Gets the guild avatar URL for this user. + /// + /// + /// This property retrieves a URL for this guild user's guild specific avatar. In event that the user does not have a valid guild avatar + /// (i.e. their avatar identifier is not set), this method will return null. + /// + /// The format to return. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// + /// + /// A string representing the user's avatar URL; null if the user does not have an avatar in place. + /// + string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); /// /// Kicks this user from this guild. /// diff --git a/src/Discord.Net.Core/Entities/Users/UserProperties.cs b/src/Discord.Net.Core/Entities/Users/UserProperties.cs index 68232b254..4cf4162a9 100644 --- a/src/Discord.Net.Core/Entities/Users/UserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/UserProperties.cs @@ -61,9 +61,13 @@ namespace Discord /// Flag given to users that developed bots and early verified their accounts. /// EarlyVerifiedBotDeveloper = 1 << 17, - /// + /// /// Flag given to users that are discord certified moderators who has give discord's exam. /// DiscordCertifiedModerator = 1 << 18, + /// + /// Flag given to bots that use only outgoing webhooks, exclusively. + /// + BotHTTPInteractions = 1 << 19, } } diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index 53f4908bf..9b888e86a 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -9,6 +9,8 @@ namespace Discord.API public User User { get; set; } [JsonProperty("nick")] public Optional Nick { get; set; } + [JsonProperty("avatar")] + public Optional Avatar { get; set; } [JsonProperty("roles")] public Optional Roles { get; set; } [JsonProperty("joined_at")] diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs index ce8c86fbf..fbb9c3e48 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs @@ -13,6 +13,8 @@ namespace Discord.API.Rest public Optional Color { get; set; } [JsonProperty("hoist")] public Optional Hoist { get; set; } + [JsonProperty("icon")] + public Optional Icon { get; set; } [JsonProperty("mentionable")] public Optional Mentionable { get; set; } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 6979d7030..cd9ab9f3d 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -373,7 +373,9 @@ namespace Discord.API Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name)); - Preconditions.LessThan(args.Name.Value.Length, 100, nameof(args.Name)); + + if(args.Name.IsSpecified) + Preconditions.LessThan(args.Name.Value.Length, 100, nameof(args.Name)); options = RequestOptions.CreateOrClone(options); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 8b0659baf..d90ba9ada 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -102,7 +102,7 @@ namespace Discord.Rest /// public string DiscoverySplashUrl => CDN.GetGuildDiscoverySplashUrl(Id, DiscoverySplashId); /// - public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId); + public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto); /// /// Gets the built-in role containing all users in this guild. diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index 8d525d4c3..1211ec40b 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -24,7 +24,8 @@ namespace Discord.Rest Hoist = args.Hoist, Mentionable = args.Mentionable, Name = args.Name, - Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue.ToString() : Optional.Create() + Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue.ToString() : Optional.Create(), + Icon = args.Icon.IsSpecified ? args.Icon.Value.ToModel() : Optional.Unspecified }; var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 556e5e124..2e184d32e 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -21,6 +21,8 @@ namespace Discord.Rest /// public string Nickname { get; private set; } + /// + public string GuildAvatarId { get; private set; } internal IGuild Guild { get; private set; } /// public bool IsDeafened { get; private set; } @@ -80,6 +82,8 @@ namespace Discord.Rest _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; + if (model.Avatar.IsSpecified) + GuildAvatarId = model.Avatar.Value; if (model.Deaf.IsSpecified) IsDeafened = model.Deaf.Value; if (model.Mute.IsSpecified) @@ -156,6 +160,9 @@ namespace Discord.Rest var guildPerms = GuildPermissions; return new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, guildPerms.RawValue)); } + + public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetGuildUserAvatarUrl(Id, GuildId, GuildAvatarId, size, format); #endregion #region IGuildUser diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index 561cd92ee..2cd19da41 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -54,6 +54,10 @@ namespace Discord.Rest /// string IGuildUser.Nickname => null; /// + string IGuildUser.GuildAvatarId => null; + /// + string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => null; + /// bool? IGuildUser.IsPending => null; /// int IGuildUser.Hierarchy => 0; diff --git a/src/Discord.Net.WebSocket/API/Gateway/GuildJoinRequestDeleteEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/GuildJoinRequestDeleteEvent.cs new file mode 100644 index 000000000..cb6fc5f40 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/GuildJoinRequestDeleteEvent.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class GuildJoinRequestDeleteEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index b7bcfdf2e..1c314bd48 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -335,6 +335,13 @@ namespace Discord.WebSocket remove { _guildUpdatedEvent.Remove(value); } } internal readonly AsyncEvent> _guildUpdatedEvent = new AsyncEvent>(); + /// Fired when a user leaves without agreeing to the member screening + public event Func GuildJoinRequestDeleted + { + add { _guildJoinRequestDeletedEvent.Add(value); } + remove { _guildJoinRequestDeletedEvent.Remove(value); } + } + internal readonly AsyncEvent> _guildJoinRequestDeletedEvent = new AsyncEvent>(); #endregion #region Users diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 613864fdc..4e2b6f324 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -485,6 +485,7 @@ namespace Discord.WebSocket client.GuildStickerCreated += (sticker) => _guildStickerCreated.InvokeAsync(sticker); client.GuildStickerDeleted += (sticker) => _guildStickerDeleted.InvokeAsync(sticker); client.GuildStickerUpdated += (before, after) => _guildStickerUpdated.InvokeAsync(before, after); + client.GuildJoinRequestDeleted += (userId, guildId) => _guildJoinRequestDeletedEvent.InvokeAsync(userId, guildId); } #endregion diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 264151e94..d2edd336a 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -74,7 +74,7 @@ namespace Discord.WebSocket internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } - internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + internal new DiscordSocketApiClient ApiClient => base.ApiClient; /// public override IReadOnlyCollection Guilds => State.Guilds; /// @@ -1004,6 +1004,14 @@ namespace Discord.WebSocket } } break; + case "GUILD_JOIN_REQUEST_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_JOIN_REQUEST_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + await TimedInvokeAsync(_guildJoinRequestDeletedEvent, nameof(GuildJoinRequestDeleted), data.UserId, data.GuildId).ConfigureAwait(false); + } + break; #endregion #region Channels diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 23712be66..cbadb9b4e 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -138,7 +138,7 @@ namespace Discord.WebSocket /// public string DiscoverySplashUrl => CDN.GetGuildDiscoverySplashUrl(Id, DiscoverySplashId); /// - public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId); + public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto); /// Indicates whether the client has all the members downloaded to the local guild cache. public bool HasAllMembers => MemberCount <= DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; /// Indicates whether the guild cache is synced to this guild. diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketAutocompleteInteractionData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketAutocompleteInteractionData.cs index d0c44bab1..44aebf22d 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketAutocompleteInteractionData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketAutocompleteInteractionData.cs @@ -60,14 +60,12 @@ namespace Discord.WebSocket { var options = new List(); + options.Add(new AutocompleteOption(model.Type, model.Name, model.Value.GetValueOrDefault(null), model.Focused.GetValueOrDefault(false))); + if (model.Options.IsSpecified) { options.AddRange(model.Options.Value.SelectMany(GetOptions)); } - else if(model.Focused.IsSpecified) - { - options.Add(new AutocompleteOption(model.Type, model.Name, model.Value.GetValueOrDefault(null), model.Focused.Value)); - } return options; } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs index 66d10fd3b..de9193e35 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -178,8 +178,8 @@ namespace Discord.WebSocket /// public override async Task FollowupWithFileAsync( Stream fileStream, + string fileName, string text = null, - string fileName = null, Embed[] embeds = null, bool isTTS = false, bool ephemeral = false, diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 8dd48891b..314471b17 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -30,7 +30,8 @@ namespace Discord.WebSocket public SocketGuild Guild { get; } /// public string Nickname { get; private set; } - + /// + public string GuildAvatarId { get; private set; } /// public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } /// @@ -154,6 +155,8 @@ namespace Discord.WebSocket _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; + if (model.Avatar.IsSpecified) + GuildAvatarId = model.Avatar.Value; if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); if (model.PremiumSince.IsSpecified) @@ -218,6 +221,8 @@ namespace Discord.WebSocket /// public ChannelPermissions GetPermissions(IGuildChannel channel) => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); + public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format); private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index c91921379..42fb807a1 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -52,6 +52,9 @@ namespace Discord.WebSocket get => GuildUser.AvatarId; internal set => GuildUser.AvatarId = value; } + /// + public string GuildAvatarId + => GuildUser.GuildAvatarId; /// public override string BannerId @@ -205,6 +208,8 @@ namespace Discord.WebSocket /// IReadOnlyCollection IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); + string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size); + internal override SocketGlobalUser GlobalUser => GuildUser.GlobalUser; internal override SocketPresence Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index d68414ebb..7cc7d5a44 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -81,6 +81,10 @@ namespace Discord.WebSocket /// string IGuildUser.Nickname => null; /// + string IGuildUser.GuildAvatarId => null; + /// + string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => null; + /// DateTimeOffset? IGuildUser.PremiumSince => null; /// bool? IGuildUser.IsPending => null;