Browse Source

Merge remote-tracking branch 'origin/SlashCommandService' into SlashCommandService

pull/1733/head
quin lynch 4 years ago
parent
commit
c5bba76124
53 changed files with 2910 additions and 41 deletions
  1. +216
    -0
      docs/guides/commands/application-commands.md
  2. +491
    -0
      src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs
  3. +1
    -1
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  4. +72
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs
  5. +49
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs
  6. +54
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs
  7. +30
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  8. +46
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  9. +29
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs
  10. +33
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs
  11. +49
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs
  12. +25
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs
  13. +43
    -0
      src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
  14. +39
    -0
      src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs
  15. +24
    -0
      src/Discord.Net.Core/Entities/Interactions/InteractionType.cs
  16. +30
    -0
      src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs
  17. +7
    -0
      src/Discord.Net.Core/Entities/Messages/MessageType.cs
  18. +23
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
  19. +21
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs
  20. +21
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs
  21. +80
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs
  22. +18
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs
  23. +31
    -0
      src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs
  24. +20
    -0
      src/Discord.Net.Rest/API/Common/InteractionFollowupMessage.cs
  25. +18
    -0
      src/Discord.Net.Rest/API/Common/InteractionResponse.cs
  26. +30
    -0
      src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
  27. +21
    -0
      src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs
  28. +21
    -0
      src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs
  29. +19
    -0
      src/Discord.Net.Rest/ClientHelper.cs
  30. +1
    -1
      src/Discord.Net.Rest/Discord.Net.Rest.csproj
  31. +161
    -5
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  32. +12
    -0
      src/Discord.Net.Rest/DiscordRestClient.cs
  33. +162
    -0
      src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
  34. +61
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs
  35. +22
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs
  36. +60
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs
  37. +14
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs
  38. +41
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs
  39. +41
    -0
      src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs
  40. +30
    -0
      src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs
  41. +37
    -0
      src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs
  42. +146
    -32
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  43. +1
    -1
      src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj
  44. +2
    -0
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  45. +84
    -1
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  46. +23
    -0
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs
  47. +12
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  48. +55
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs
  49. +32
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs
  50. +70
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs
  51. +215
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
  52. +39
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs
  53. +28
    -0
      src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs

+ 216
- 0
docs/guides/commands/application-commands.md View File

@@ -0,0 +1,216 @@
# Application commands

Application commands are a new feature thats still a work in progress, this guide will show you how to make the best of em.




## Getting started

### Configuring

There is a new configuration setting for your DiscordSocketClient called `AlwaysAcknowledgeInteractions`, It's default value is true.
Interactions work off of the Recieve -> Respond pipeline, meaning if you dont acknowledge the interaction within 3 seconds its gone forever.
With `AlwaysAcknowledgeInteractions` set to true, the client will automatically acknowledge the interaction as its recieved,
letting you wait up to 15 minutes before responding with a message.

With `AlwaysAcknowledgeInteractions` set to false you will have to acknowledge the interaction yourself via the `InteractionCreated` event

### Registering commands

While there is no "easy" way to register command right now, in the future I plan to write a command service to help with that, but right now you have to use the rest
client to create your command:

```cs
_client.Ready += RegisterCommands

...

private async Task RegisterCommands()
{
// Creating a global command
var myGlobalCommand = await _client.Rest.CreateGlobalCommand(new Discord.SlashCommandCreationProperties()
{
Name = "example",
Description = "Runs the example command",
Options = new List<Discord.ApplicationCommandOptionProperties>()
{
new ApplicationCommandOptionProperties()
{
Name = "Example option",
Required = false,
Description = "Option Description",
Type = Discord.ApplicationCommandOptionType.String,
}
}
});

// Creating a guild command
var myGuildCommand = await _client.Rest.CreateGuildCommand(new Discord.SlashCommandCreationProperties()
{
Name = "guildExample",
Description = "Runs the guild example command",
Options = new List<Discord.ApplicationCommandOptionProperties>()
{
new ApplicationCommandOptionProperties()
{
Name = "Guild example option",
Required = false,
Description = "Guild option description",
Type = Discord.ApplicationCommandOptionType.String,
}
}
}, 1234567890); // <- the guild id
}
```
CreateGuildCommand returns a `RestGuildCommand` class which can be used to modify/delete your command on the fly, it also contains details about your command.
CreateGlobalCOmmand returns a `RestGlobalCommand` class which can be used to modify/delete your command on the fly, it also contains details about your command.

### Getting a list of all your commands
You can fetch a list of all your global commands via rest:
```cs
var commands = _client.Rest.GetGlobalApplicationCommands();
```
This returns a `IReadOnlyCollection<RestGlobalCommand>`.

You can also fetch guild specific commands:
```cs
var commands = _client.Rest.GetGuildApplicationCommands(1234567890)
```
This returns all the application commands in that guild.

### Responding

First thing we want to do is listen to the `InteractionCreated` event. This event is fired when a socket interaction is recieved via the gateway, It looks somthing like this
```cs
_client.InteractionCreated += MyEventHandler;

...

private async Task MyEventHandler(SocketInteraction arg)
{
// handle the interaction here
}
```

A socket interaction is made up of these properties and methods:

| Name | Description |
|--------|--------------|
| Guild | The `SocketGuild` this interaction was used in |
| Channel | The `SocketTextChannel` this interaction was used in |
| Member | The `SocketGuildUser` that executed the interaction |
| Type | The [InteractionType](https://discord.com/developers/docs/interactions/slash-commands#interaction-interactiontype) of this interaction |
| Data | The `SocketInteractionData` associated with this interaction |
| Token | The token used to respond to this interaction |
| Version | The version of this interaction |
| CreatedAt | The time this interaction was created |
| IsValidToken | Whether or not the token to respond to this interaction is still valid |
| RespondAsync | Responds to the interaction |
| FollowupAsync | Sends a followup message to the interaction |



#### Whats the difference between `FollowupAsync` and `RespondAsync`?
RespondAsync is the initial responce to the interaction, its used to "capture" the interaction, while followup is used to send more messages to the interaction.
Basically, you want to first use `RespondAsync` to acknowledge the interaction, then if you need to send anything else regarding that interaction you would use `FollowupAsync`
If you have `AlwaysAcknowledgeInteractions` set to true in your client config then it will automatically acknowledge the interaction without sending a message,
in this case you can use either or to respond.

#### Example ping pong command
```cs
_client.InteractionCreated += MyEventHandler;
_client.Ready += CreateCommands

...

private async Task CreateCommands()
{
await _client.Rest.CreateGlobalCommand(new Discord.SlashCommandCreationProperties()
{
Name = "ping",
Description = "ping for a pong!",
});
}

private async Task MyEventHandler(SocketInteraction arg)
{
switch(arg.Type) // We want to check the type of this interaction
{
case InteractionType.ApplicationCommand: // If it is a command
await MySlashCommandHandler(arg); // Handle the command somewhere
break;
default: // We dont support it
Console.WriteLine("Unsupported interaction type: " + arg.Type);
break;
}
}

private async Task MySlashCommandHandler(SocketInteraction arg)
{
switch(arg.Name)
{
case "ping":
await arg.RespondAsync("Pong!");
break;
}
}
```

#### Example hug command
```cs
_client.InteractionCreated += MyEventHandler;
_client.Ready += CreateCommands;

...

private async Task CreateCommands()
{
await _client.Rest.CreateGlobalCommand(new Discord.SlashCommandCreationProperties()
{
Name = "hug",
Description = "Hugs a user!",
Options = new List<Discord.ApplicationCommandOptionProperties>()
{
new ApplicationCommandOptionProperties()
{
Name = "User",
Required = true,
Description = "The user to hug",
Type = Discord.ApplicationCommandOptionType.User,
}
}
});
}

private async Task MyEventHandler(SocketInteraction arg)
{
switch(arg.Type) // We want to check the type of this interaction
{
case InteractionType.ApplicationCommand: // If it is a command
await MySlashCommandHandler(arg); // Handle the command somewhere
break;
default: // We dont support it
Console.WriteLine("Unsupported interaction type: " + arg.Type);
break;
}
}

private async Task MySlashCommandHandler(SocketInteraction arg)
{
switch(arg.Name)
{
case "hug":
// Get the user argument
var option = arg.Data.Options.First(x => x.Name == "user");
// We know that the options value must be a user
if(option.Value is SocketGuildUser user)
{
await arg.RespondAsync($"Hugged {user.Mention}");
}
break;
}
}
```



+ 491
- 0
src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs View File

@@ -0,0 +1,491 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Discord.Commands.Builders
{
/// <summary>
/// A class used to build slash commands.
/// </summary>
public class SlashCommandBuilder
{
/// <summary>
/// Returns the maximun length a commands name allowed by Discord
/// </summary>
public const int MaxNameLength = 32;
/// <summary>
/// Returns the maximum length of a commands description allowed by Discord.
/// </summary>
public const int MaxDescriptionLength = 100;
/// <summary>
/// Returns the maximum count of command options allowed by Discord
/// </summary>
public const int MaxOptionsCount = 10;

/// <summary>
/// The name of this slash command.
/// </summary>
public string Name
{
get
{
return _name;
}
set
{
Preconditions.NotNullOrEmpty(value, nameof(Name));
Preconditions.AtLeast(value.Length, 3, nameof(Name));
Preconditions.AtMost(value.Length, MaxNameLength, nameof(Name));

// Discord updated the docs, this regex prevents special characters like @!$%(... etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(value, @"^[\w-]{3,32}$"))
throw new ArgumentException("Command name cannot contian any special characters or whitespaces!");

_name = value;
}
}

/// <summary>
/// A 1-100 length description of this slash command
/// </summary>
public string Description
{
get
{
return _description;
}
set
{
Preconditions.AtLeast(value.Length, 1, nameof(Description));
Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description));

_description = value;
}
}

public ulong GuildId
{
get
{
return _guildId ?? 0;
}
set
{
if (value == 0)
{
throw new ArgumentException("Guild ID cannot be 0!");
}

_guildId = value;

if (isGlobal)
isGlobal = false;
}
}
/// <summary>
/// Gets or sets the options for this command.
/// </summary>
public List<SlashCommandOptionBuilder> Options
{
get
{
return _options;
}
set
{
if (value != null)
if (value.Count > MaxOptionsCount)
throw new ArgumentException(message: $"Option count must be less than or equal to {MaxOptionsCount}.", paramName: nameof(Options));

_options = value;
}
}

private ulong? _guildId { get; set; }
private string _name { get; set; }
private string _description { get; set; }
private List<SlashCommandOptionBuilder> _options { get; set; }

internal bool isGlobal { get; set; }


public SlashCommandCreationProperties Build()
{
SlashCommandCreationProperties props = new SlashCommandCreationProperties()
{
Name = this.Name,
Description = this.Description,
};

if(this.Options != null || this.Options.Any())
{
var options = new List<ApplicationCommandOptionProperties>();

this.Options.ForEach(x => options.Add(x.Build()));

props.Options = options;
}

return props;

}

/// <summary>
/// Makes this command a global application command .
/// </summary>
/// <returns>The current builder.</returns>
public SlashCommandBuilder MakeGlobal()
{
this.isGlobal = true;
return this;
}

/// <summary>
/// Makes this command a guild specific command.
/// </summary>
/// <param name="GuildId">The Id of the target guild.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder ForGuild(ulong GuildId)
{
this.GuildId = GuildId;
return this;
}

public SlashCommandBuilder WithName(string Name)
{
this.Name = Name;
return this;
}

/// <summary>
/// Sets the description of the current command.
/// </summary>
/// <param name="Description">The description of this command.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder WithDescription(string Description)
{
this.Description = Description;
return this;
}

/// <summary>
/// Adds an option to the current slash command.
/// </summary>
/// <param name="Name">The name of the option to add.</param>
/// <param name="Type">The type of this option.</param>
/// <param name="Description">The description of this option.</param>
/// <param name="Required">If this option is required for this command.</param>
/// <param name="Default">If this option is the default option.</param>
/// <param name="Options">The options of the option to add.</param>
/// <param name="Choices">The choices of this option.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type,
string Description, bool Required = true, bool Default = false, List<SlashCommandOptionBuilder> Options = null, params ApplicationCommandOptionChoiceProperties[] Choices)
{
// Make sure the name matches the requirements from discord
Preconditions.NotNullOrEmpty(Name, nameof(Name));
Preconditions.AtLeast(Name.Length, 3, nameof(Name));
Preconditions.AtMost(Name.Length, MaxNameLength, nameof(Name));

// Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(Name, @"^[\w-]{3,32}$"))
throw new ArgumentException("Command name cannot contian any special characters or whitespaces!", nameof(Name));

// same with description
Preconditions.NotNullOrEmpty(Description, nameof(Description));
Preconditions.AtLeast(Description.Length, 3, nameof(Description));
Preconditions.AtMost(Description.Length, MaxDescriptionLength, nameof(Description));

// make sure theres only one option with default set to true
if (Default)
{
if (this.Options != null)
if (this.Options.Any(x => x.Default))
throw new ArgumentException("There can only be one command option with default set to true!", nameof(Default));
}

SlashCommandOptionBuilder option = new SlashCommandOptionBuilder();
option.Name = Name;
option.Description = Description;
option.Required = Required;
option.Default = Default;
option.Options = Options;
option.Choices = Choices != null ? new List<ApplicationCommandOptionChoiceProperties>(Choices) : null;

return AddOption(option);
}

/// <summary>
/// Adds an option to the current slash command.
/// </summary>
/// <param name="Name">The name of the option to add.</param>
/// <param name="Type">The type of this option.</param>
/// <param name="Description">The description of this option.</param>
/// <param name="Required">If this option is required for this command.</param>
/// <param name="Default">If this option is the default option.</param>
/// <param name="Choices">The choices of this option.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type,
string Description, bool Required = true, bool Default = false, params ApplicationCommandOptionChoiceProperties[] Choices)
=> AddOption(Name, Type, Description, Required, Default, null, Choices);

/// <summary>
/// Adds an option to the current slash command.
/// </summary>
/// <param name="Name">The name of the option to add.</param>
/// <param name="Type">The type of this option.</param>
/// <param name="Description">The sescription of this option.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type, string Description)
=> AddOption(Name, Type, Description, Options: null, Choices: null);

/// <summary>
/// Adds an option to this slash command.
/// </summary>
/// <param name="Parameter">The option to add.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder AddOption(SlashCommandOptionBuilder Option)
{
if (this.Options == null)
this.Options = new List<SlashCommandOptionBuilder>();

if (this.Options.Count >= MaxOptionsCount)
throw new ArgumentOutOfRangeException(nameof(Options), $"Cannot have more than {MaxOptionsCount} options!");

if (Option == null)
throw new ArgumentNullException(nameof(Option), "Option cannot be null");

this.Options.Add(Option);
return this;
}
/// <summary>
/// Adds a collection of options to the current slash command.
/// </summary>
/// <param name="Parameter">The collection of options to add.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder AddOptions(params SlashCommandOptionBuilder[] Options)
{
if (Options == null)
throw new ArgumentNullException(nameof(Options), "Options cannot be null!");

if (Options.Length == 0)
throw new ArgumentException(nameof(Options), "Options cannot be empty!");

if (this.Options == null)
this.Options = new List<SlashCommandOptionBuilder>();

if (this.Options.Count + Options.Length > MaxOptionsCount)
throw new ArgumentOutOfRangeException(nameof(Options), $"Cannot have more than {MaxOptionsCount} options!");

this.Options.AddRange(Options);
return this;
}
}

/// <summary>
/// Represents a class used to build options for the <see cref="SlashCommandBuilder"/>.
/// </summary>
public class SlashCommandOptionBuilder
{
/// <summary>
/// The max length of a choice's name allowed by Discord.
/// </summary>
public const int ChoiceNameMaxLength = 100;

/// <summary>
/// The maximum number of choices allowed by Discord.
/// </summary>
public const int MaxChoiceCount = 10;

private string _name;
private string _description;

/// <summary>
/// The name of this option.
/// </summary>
public string Name
{
get => _name;
set
{
if (value?.Length > SlashCommandBuilder.MaxNameLength)
throw new ArgumentException("Name length must be less than or equal to 32");
if(value?.Length < 3)
throw new ArgumentException("Name length must at least 3 characters in length");

// Discord updated the docs, this regex prevents special characters like @!$%(... etc,
// https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
if (!Regex.IsMatch(value, @"^[\w-]{3,32}$"))
throw new ArgumentException("Option name cannot contian any special characters or whitespaces!");

_name = value;
}
}

/// <summary>
/// The description of this option.
/// </summary>
public string Description
{
get => _description;
set
{
if (value?.Length > SlashCommandBuilder.MaxDescriptionLength)
throw new ArgumentException("Description length must be less than or equal to 100");
if (value?.Length < 1)
throw new ArgumentException("Name length must at least 1 character in length");

_description = value;
}
}

/// <summary>
/// The type of this option.
/// </summary>
public ApplicationCommandOptionType Type { get; set; }

/// <summary>
/// The first required option for the user to complete. only one option can be default.
/// </summary>
public bool Default { get; set; }

/// <summary>
/// <see langword="true"/> if this option is required for this command, otherwise <see langword="false"/>.
/// </summary>
public bool Required { get; set; }

/// <summary>
/// choices for string and int types for the user to pick from.
/// </summary>
public List<ApplicationCommandOptionChoiceProperties> Choices { get; set; }

/// <summary>
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters.
/// </summary>
public List<SlashCommandOptionBuilder> Options { get; set; }

/// <summary>
/// Builds the current option.
/// </summary>
/// <returns>The build version of this option</returns>
public ApplicationCommandOptionProperties Build()
{
bool isSubType = this.Type == ApplicationCommandOptionType.SubCommand || this.Type == ApplicationCommandOptionType.SubCommandGroup;

if (isSubType && (Options == null || !Options.Any()))
throw new ArgumentException(nameof(Options), "SubCommands/SubCommandGroups must have at least one option");

if (!isSubType && (Options != null && Options.Any()))
throw new ArgumentException(nameof(Options), $"Cannot have options on {Type} type");

return new ApplicationCommandOptionProperties()
{
Name = this.Name,
Description = this.Description,
Default = this.Default,
Required = this.Required,
Type = this.Type,
Options = new List<ApplicationCommandOptionProperties>(this.Options.Select(x => x.Build())),
Choices = this.Choices
};
}

/// <summary>
/// Adds a sub
/// </summary>
/// <param name="option"></param>
/// <returns></returns>
public SlashCommandOptionBuilder AddOption(SlashCommandOptionBuilder option)
{
if (this.Options == null)
this.Options = new List<SlashCommandOptionBuilder>();

if (this.Options.Count >= SlashCommandBuilder.MaxOptionsCount)
throw new ArgumentOutOfRangeException(nameof(Choices), $"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!");

if (option == null)
throw new ArgumentNullException(nameof(option), "Option cannot be null");

Options.Add(option);
return this;
}

public SlashCommandOptionBuilder AddChoice(string Name, int Value)
{
if (Choices == null)
Choices = new List<ApplicationCommandOptionChoiceProperties>();

if (Choices.Count >= MaxChoiceCount)
throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!");

Choices.Add(new ApplicationCommandOptionChoiceProperties()
{
Name = Name,
Value = Value
});

return this;
}
public SlashCommandOptionBuilder AddChoice(string Name, string Value)
{
if (Choices == null)
Choices = new List<ApplicationCommandOptionChoiceProperties>();

if (Choices.Count >= MaxChoiceCount)
throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!");

Choices.Add(new ApplicationCommandOptionChoiceProperties()
{
Name = Name,
Value = Value
});

return this;
}

public SlashCommandOptionBuilder WithName(string Name, int Value)
{
if (Choices == null)
Choices = new List<ApplicationCommandOptionChoiceProperties>();

if (Choices.Count >= MaxChoiceCount)
throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!");

Choices.Add(new ApplicationCommandOptionChoiceProperties()
{
Name = Name,
Value = Value
});

return this;
}

public SlashCommandOptionBuilder WithDescription(string Description)
{
this.Description = Description;
return this;
}

public SlashCommandOptionBuilder WithRequired(bool value)
{
this.Required = value;
return this;
}

public SlashCommandOptionBuilder WithDefault(bool value)
{
this.Default = value;
return this;
}
public SlashCommandOptionBuilder WithType(ApplicationCommandOptionType Type)
{
this.Type = Type;
return this;
}
}
}

+ 1
- 1
src/Discord.Net.Commands/Discord.Net.Commands.csproj View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets"/>
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Commands</AssemblyName>
<RootNamespace>Discord.Commands</RootNamespace>


+ 72
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOption.cs View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a <see cref="IApplicationCommandOption"/> for making slash commands.
/// </summary>
public class ApplicationCommandOptionProperties
{
private string _name;
private string _description;

/// <summary>
/// The name of this option.
/// </summary>
public string Name
{
get => _name;
set
{
if (value?.Length > 32)
throw new ArgumentException("Name length must be less than or equal to 32");
_name = value;
}
}

/// <summary>
/// The description of this option.
/// </summary>
public string Description
{
get => _description;
set
{
if (value?.Length > 100)
throw new ArgumentException("Name length must be less than or equal to 32");
_description = value;
}
}

/// <summary>
/// The type of this option.
/// </summary>
public ApplicationCommandOptionType Type { get; set; }

/// <summary>
/// The first required option for the user to complete. only one option can be default.
/// </summary>
public bool? Default { get; set; }

/// <summary>
/// <see langword="true"/> if this option is required for this command, otherwise <see langword="false"/>.
/// </summary>
public bool? Required { get; set; }

/// <summary>
/// choices for string and int types for the user to pick from
/// </summary>
public List<ApplicationCommandOptionChoiceProperties> Choices { get; set; }

/// <summary>
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters.
/// </summary>
public List<ApplicationCommandOptionProperties> Options { get; set; }

}
}

+ 49
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionChoice.cs View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a choice for a <see cref="IApplicationCommandInteractionDataOption"/>. This class is used when making new commands
/// </summary>
public class ApplicationCommandOptionChoiceProperties
{
private string _name;
private object _value;
/// <summary>
/// The name of this choice
/// </summary>
public string Name
{
get => _name;
set
{
if(value?.Length > 100)
throw new ArgumentException("Name length must be less than or equal to 100");
_name = value;
}
}

// Note: discord allows strings & ints as values. how should that be handled?
// should we make this an object and then just type check it?
/// <summary>
/// The value of this choice
/// </summary>
public object Value
{
get => _value;
set
{
if(value != null)
{
if(!(value is int) && !(value is string))
throw new ArgumentException("The value of a choice must be a string or int!");
}
_value = value;
}
}
}
}

+ 54
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// The option type of the Slash command parameter, See <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype">the discord docs</see>.
/// </summary>
public enum ApplicationCommandOptionType : byte
{
/// <summary>
/// A sub command.
/// </summary>
SubCommand = 1,

/// <summary>
/// A group of sub commands.
/// </summary>
SubCommandGroup = 2,

/// <summary>
/// A <see langword="string"/> of text.
/// </summary>
String = 3,

/// <summary>
/// An <see langword="int"/>.
/// </summary>
Integer = 4,

/// <summary>
/// A <see langword="bool"/>.
/// </summary>
Boolean = 5,

/// <summary>
/// A <see cref="IGuildUser"/>.
/// </summary>
User = 6,

/// <summary>
/// A <see cref="IGuildChannel"/>.
/// </summary>
Channel = 7,

/// <summary>
/// A <see cref="IRole"/>.
/// </summary>
Role = 8
}
}

+ 30
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Provides properties that are used to modify a <see cref="IApplicationCommand" /> with the specified changes.
/// </summary>
public class ApplicationCommandProperties
{
/// <summary>
/// Gets or sets the name of this command.
/// </summary>
public Optional<string> Name { get; set; }

/// <summary>
/// Gets or sets the discription of this command.
/// </summary>
public Optional<string> Description { get; set; }

/// <summary>
/// Gets or sets the options for this command.
/// </summary>
public Optional<List<ApplicationCommandOptionProperties>> Options { get; set; }
}
}

+ 46
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// The base command model that belongs to an application. see <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommand"/>
/// </summary>
public interface IApplicationCommand : ISnowflakeEntity
{
/// <summary>
/// Gets the unique id of the command.
/// </summary>
ulong Id { get; }

/// <summary>
/// Gets the unique id of the parent application.
/// </summary>
ulong ApplicationId { get; }

/// <summary>
/// The name of the command.
/// </summary>
string Name { get; }

/// <summary>
/// The description of the command.
/// </summary>
string Description { get; }

/// <summary>
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters.
/// </summary>
IReadOnlyCollection<IApplicationCommandOption> Options { get; }

/// <summary>
/// Deletes this command
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns></returns>
Task DeleteAsync(RequestOptions options = null);
}
}

+ 29
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionData.cs View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents data of an Interaction Command, see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondata"/>
/// </summary>
public interface IApplicationCommandInteractionData
{
/// <summary>
/// The snowflake id of this command
/// </summary>
ulong Id { get; }

/// <summary>
/// The name of this command
/// </summary>
string Name { get; }

/// <summary>
/// The params + values from the user
/// </summary>
IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; }
}
}

+ 33
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandInteractionDataOption.cs View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a option group for a command, see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondataoption"/>
/// </summary>
public interface IApplicationCommandInteractionDataOption
{
/// <summary>
/// The name of the parameter.
/// </summary>
string Name { get; }

/// <summary>
/// The value of the pair.
/// <note>
/// This objects type can be any one of the option types in <see cref="ApplicationCommandOptionType"/>
/// </note>
/// </summary>
object Value { get; }

/// <summary>
/// Present if this option is a group or subcommand.
/// </summary>
IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; }

}
}

+ 49
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOption.cs View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Options for the <see cref="IApplicationCommand"/>, see <see href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoption"/>The docs</see>.
/// </summary>
public interface IApplicationCommandOption
{
/// <summary>
/// The type of this <see cref="IApplicationCommandOption"/>.
/// </summary>
ApplicationCommandOptionType Type { get; }

/// <summary>
/// The name of this command option, 1-32 character name.
/// </summary>
string Name { get; }

/// <summary>
/// The discription of this command option, 1-100 character description.
/// </summary>
string Description { get; }

/// <summary>
/// The first required option for the user to complete--only one option can be default.
/// </summary>
bool? Default { get; }

/// <summary>
/// If the parameter is required or optional, default is <see langword="false"/>.
/// </summary>
bool? Required { get; }

/// <summary>
/// Choices for string and int types for the user to pick from.
/// </summary>
IReadOnlyCollection<IApplicationCommandOptionChoice>? Choices { get; }

/// <summary>
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters.
/// </summary>
IReadOnlyCollection<IApplicationCommandOption>? Options { get; }
}
}

+ 25
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommandOptionChoice.cs View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Specifies choices for command group.
/// </summary>
public interface IApplicationCommandOptionChoice
{
/// <summary>
/// 1-100 character choice name.
/// </summary>
string Name { get; }

/// <summary>
/// value of the choice.
/// </summary>
object Value { get; }

}
}

+ 43
- 0
src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a discord interaction
/// <para>
/// An interaction is the base "thing" that is sent when a user invokes a command, and is the same for Slash Commands
/// and other future interaction types. see <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction"/>.
/// </para>
/// </summary>
public interface IDiscordInteraction : ISnowflakeEntity
{
/// <summary>
/// The id of the interaction.
/// </summary>
ulong Id { get; }

/// <summary>
/// The type of this <see cref="IDiscordInteraction"/>.
/// </summary>
InteractionType Type { get; }

/// <summary>
/// The command data payload.
/// </summary>
IApplicationCommandInteractionData? Data { get; }

/// <summary>
/// A continuation token for responding to the interaction.
/// </summary>
string Token { get; }

/// <summary>
/// read-only property, always 1.
/// </summary>
int Version { get; }
}
}

+ 39
- 0
src/Discord.Net.Core/Entities/Interactions/InteractionResponseType.cs View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// The response type for an <see cref="IDiscordInteraction"/>.
/// </summary>
public enum InteractionResponseType : byte
{
/// <summary>
/// ACK a Ping.
/// </summary>
Pong = 1,

/// <summary>
/// ACK a command without sending a message, eating the user's input.
/// </summary>
Acknowledge = 2,

/// <summary>
/// Respond with a message, eating the user's input.
/// </summary>
ChannelMessage = 3,

/// <summary>
/// Respond with a message, showing the user's input.
/// </summary>
ChannelMessageWithSource = 4,

/// <summary>
/// ACK a command without sending a message, showing the user's input.
/// </summary>
ACKWithSource = 5
}
}

+ 24
- 0
src/Discord.Net.Core/Entities/Interactions/InteractionType.cs View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// Represents a type of Interaction from discord.
/// </summary>
public enum InteractionType : byte
{
/// <summary>
/// A ping from discord.
/// </summary>
Ping = 1,

/// <summary>
/// An <see cref="IApplicationCommand"/> sent from discord.
/// </summary>
ApplicationCommand = 2
}
}

+ 30
- 0
src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
{
/// <summary>
/// A class used to create slash commands
/// </summary>
public class SlashCommandCreationProperties
{
/// <summary>
/// The name of this command.
/// </summary>
public string Name { get; set; }

/// <summary>
/// The discription of this command.
/// </summary>
public string Description { get; set; }


/// <summary>
/// Gets or sets the options for this command.
/// </summary>
public Optional<List<ApplicationCommandOptionProperties>> Options { get; set; }
}
}

+ 7
- 0
src/Discord.Net.Core/Entities/Messages/MessageType.cs View File

@@ -64,5 +64,12 @@ namespace Discord
/// Only available in API v8.
/// </remarks>
Reply = 19,
/// <summary>
/// The message is an Application Command
/// </summary>
/// <remarks>
/// Only available in API v8
/// </remarks>
ApplicationCommand = 20
}
}

+ 23
- 0
src/Discord.Net.Rest/API/Common/ApplicationCommand.cs View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class ApplicationCommand
{
[JsonProperty("id")]
public ulong Id { get; set; }
[JsonProperty("application_id")]
public ulong ApplicationId { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("options")]
public Optional<ApplicationCommandOption[]> Options { get; set; }
}
}

+ 21
- 0
src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs View File

@@ -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
{
internal class ApplicationCommandInteractionData
{
[JsonProperty("id")]
public ulong Id { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("options")]
public Optional<ApplicationCommandInteractionDataOption[]> Options { get; set; }
}
}

+ 21
- 0
src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataOption.cs View File

@@ -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
{
internal class ApplicationCommandInteractionDataOption
{
[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("value")]
public Optional<object> Value { get; set; }

[JsonProperty("options")]
public Optional<IEnumerable<ApplicationCommandInteractionDataOption>> Options { get; set; }
}
}

+ 80
- 0
src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs View File

@@ -0,0 +1,80 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class ApplicationCommandOption
{
[JsonProperty("type")]
public ApplicationCommandOptionType Type { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("description")]
public string Description { get; set; }

[JsonProperty("default")]
public Optional<bool> Default { get; set; }

[JsonProperty("required")]
public Optional<bool> Required { get; set; }

[JsonProperty("choices")]
public Optional<ApplicationCommandOptionChoice[]> Choices { get; set; }

[JsonProperty("options")]
public Optional<ApplicationCommandOption[]> Options { get; set; }

public ApplicationCommandOption() { }

public ApplicationCommandOption(IApplicationCommandOption cmd)
{
this.Choices = cmd.Choices.Select(x => new ApplicationCommandOptionChoice()
{
Name = x.Name,
Value = x.Value
}).ToArray();

this.Options = cmd.Options.Select(x => new ApplicationCommandOption(x)).ToArray();

this.Required = cmd.Required.HasValue
? cmd.Required.Value
: Optional<bool>.Unspecified;
this.Default = cmd.Default.HasValue
? cmd.Default.Value
: Optional<bool>.Unspecified;

this.Name = cmd.Name;
this.Type = cmd.Type;
this.Description = cmd.Description;
}
public ApplicationCommandOption(Discord.ApplicationCommandOptionProperties option)
{
this.Choices = option.Choices != null
? option.Choices.Select(x => new ApplicationCommandOptionChoice()
{
Name = x.Name,
Value = x.Value
}).ToArray()
: Optional<ApplicationCommandOptionChoice[]>.Unspecified;

this.Options = option.Options != null
? option.Options.Select(x => new ApplicationCommandOption(x)).ToArray()
: Optional<ApplicationCommandOption[]>.Unspecified;

this.Required = option.Required.Value;
this.Default = option.Default.HasValue
? option.Default.Value
: Optional<bool>.Unspecified;

this.Name = option.Name;
this.Type = option.Type;
this.Description = option.Description;
}
}
}

+ 18
- 0
src/Discord.Net.Rest/API/Common/ApplicationCommandOptionChoice.cs View File

@@ -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
{
internal class ApplicationCommandOptionChoice
{
[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("value")]
public object Value { get; set; }
}
}

+ 31
- 0
src/Discord.Net.Rest/API/Common/InteractionApplicationCommandCallbackData.cs View File

@@ -0,0 +1,31 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class InteractionApplicationCommandCallbackData
{
[JsonProperty("tts")]
public Optional<bool> TTS { get; set; }

[JsonProperty("content")]
public Optional<string> Content { get; set; }

[JsonProperty("embeds")]
public Optional<Embed[]> Embeds { get; set; }

[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }

public InteractionApplicationCommandCallbackData() { }
public InteractionApplicationCommandCallbackData(string text)
{
this.Content = text;
}
}
}

+ 20
- 0
src/Discord.Net.Rest/API/Common/InteractionFollowupMessage.cs View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API
{
internal class InteractionFollowupMessage
{
public string Content { get; set; }
public Optional<string> Username { get; set; }
public Optional<string> AvatarUrl { get; set; }
public Optional<bool> TTS { get; set; }
public Optional<Stream> File { get; set; }
public Embed[] Embeds { get; set; }

}
}

+ 18
- 0
src/Discord.Net.Rest/API/Common/InteractionResponse.cs View File

@@ -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
{
internal class InteractionResponse
{
[JsonProperty("type")]
public InteractionResponseType Type { get; set; }

[JsonProperty("data")]
public Optional<InteractionApplicationCommandCallbackData> Data { get; set; }
}
}

+ 30
- 0
src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs View File

@@ -0,0 +1,30 @@
using Discord.API;
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 CreateApplicationCommandParams
{
[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("description")]
public string Description { get; set; }

[JsonProperty("options")]
public Optional<ApplicationCommandOption[]> Options { get; set; }

public CreateApplicationCommandParams() { }
public CreateApplicationCommandParams(string name, string description, ApplicationCommandOption[] options = null)
{
this.Name = name;
this.Description = description;
this.Options = Optional.Create<ApplicationCommandOption[]>(options);
}
}
}

+ 21
- 0
src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs View File

@@ -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.Rest
{
internal class ModifyApplicationCommandParams
{
[JsonProperty("name")]
public Optional<string> Name { get; set; }

[JsonProperty("description")]
public Optional<string> Description { get; set; }

[JsonProperty("options")]
public Optional<ApplicationCommandOption[]> Options { get; set; }
}
}

+ 21
- 0
src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs View File

@@ -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.Rest
{
internal class ModifyInteractionResponseParams
{
[JsonProperty("content")]
public string Content { get; set; }

[JsonProperty("embeds")]
public Optional<Embed[]> Embeds { get; set; }

[JsonProperty("allowed_mentions")]
public Optional<AllowedMentions> AllowedMentions { get; set; }
}
}

+ 19
- 0
src/Discord.Net.Rest/ClientHelper.cs View File

@@ -201,5 +201,24 @@ namespace Discord.Rest
}
};
}

public static async Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommands(BaseDiscordClient client, RequestOptions options)
{
var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false);

if (!response.Any())
return new RestGlobalCommand[0];

return response.Select(x => RestGlobalCommand.Create(client, x)).ToArray();
}
public static async Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommands(BaseDiscordClient client, ulong guildId, RequestOptions options)
{
var response = await client.ApiClient.GetGuildApplicationCommandAsync(guildId, options).ConfigureAwait(false);

if (!response.Any())
return new RestGuildCommand[0].ToImmutableArray();

return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToImmutableArray();
}
}
}

+ 1
- 1
src/Discord.Net.Rest/Discord.Net.Rest.csproj View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets"/>
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Rest</AssemblyName>
<RootNamespace>Discord.Rest</RootNamespace>


+ 161
- 5
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -46,8 +46,7 @@ namespace Discord.API
internal IRestClient RestClient { get; private set; }
internal ulong? CurrentUserId { get; set; }
public RateLimitPrecision RateLimitPrecision { get; private set; }
internal bool UseSystemClock { get; set; }

internal bool UseSystemClock { get; set; }
internal JsonSerializer Serializer => _serializer;

/// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
@@ -59,7 +58,7 @@ namespace Discord.API
DefaultRetryMode = defaultRetryMode;
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
RateLimitPrecision = rateLimitPrecision;
UseSystemClock = useSystemClock;
UseSystemClock = useSystemClock;

RequestQueue = new RequestQueue();
_stateLock = new SemaphoreSlim(1, 1);
@@ -263,8 +262,8 @@ namespace Discord.API
CheckState();
if (request.Options.RetryMode == null)
request.Options.RetryMode = DefaultRetryMode;
if (request.Options.UseSystemClock == null)
request.Options.UseSystemClock = UseSystemClock;
if (request.Options.UseSystemClock == null)
request.Options.UseSystemClock = UseSystemClock;

var stopwatch = Stopwatch.StartNew();
var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false);
@@ -786,6 +785,163 @@ namespace Discord.API
await SendAsync("DELETE", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false);
}

//Interactions
public async Task<ApplicationCommand[]> GetGlobalApplicationCommandsAsync(RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);

return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{this.CurrentUserId}/commands", new BucketIds(), options: options).ConfigureAwait(false);
}


public async Task<ApplicationCommand> CreateGlobalApplicationCommandAsync(CreateApplicationCommandParams command, RequestOptions options = null)
{
Preconditions.NotNull(command, nameof(command));
Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name));
Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name));
Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description));
Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description));

options = RequestOptions.CreateOrClone(options);

return await SendJsonAsync<ApplicationCommand>("POST", () => $"applications/{this.CurrentUserId}/commands", command, new BucketIds(), options: options).ConfigureAwait(false);
}
public async Task<ApplicationCommand> ModifyGlobalApplicationCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null)
{
Preconditions.NotNull(command, nameof(command));

if (command.Name.IsSpecified)
{
Preconditions.AtMost(command.Name.Value.Length, 32, nameof(command.Name));
Preconditions.AtLeast(command.Name.Value.Length, 3, nameof(command.Name));
}
if (command.Description.IsSpecified)
{
Preconditions.AtMost(command.Description.Value.Length, 100, nameof(command.Description));
Preconditions.AtLeast(command.Description.Value.Length, 1, nameof(command.Description));
}

options = RequestOptions.CreateOrClone(options);

return await SendJsonAsync<ApplicationCommand>("PATCH", () => $"applications/{this.CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options).ConfigureAwait(false);
}
public async Task DeleteGlobalApplicationCommandAsync(ulong commandId, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);

await SendAsync("DELETE", () => $"applications/{this.CurrentUserId}/commands/{commandId}", new BucketIds(), options: options).ConfigureAwait(false);
}

public async Task<ApplicationCommand[]> GetGuildApplicationCommandAsync(ulong guildId, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds(guildId: guildId);

return await SendAsync<ApplicationCommand[]>("GET", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", bucket, options: options).ConfigureAwait(false);
}
public async Task<ApplicationCommand> CreateGuildApplicationCommandAsync(CreateApplicationCommandParams command, ulong guildId, RequestOptions options = null)
{
Preconditions.NotNull(command, nameof(command));
Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name));
Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name));
Preconditions.AtMost(command.Description.Length, 100, nameof(command.Description));
Preconditions.AtLeast(command.Description.Length, 1, nameof(command.Description));

options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds(guildId: guildId);

return await SendJsonAsync<ApplicationCommand>("POST", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, bucket, options: options).ConfigureAwait(false);
}
public async Task<ApplicationCommand> ModifyGuildApplicationCommandAsync(ModifyApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null)
{
Preconditions.NotNull(command, nameof(command));

if (command.Name.IsSpecified)
{
Preconditions.AtMost(command.Name.Value.Length, 32, nameof(command.Name));
Preconditions.AtLeast(command.Name.Value.Length, 3, nameof(command.Name));
}
if (command.Description.IsSpecified)
{
Preconditions.AtMost(command.Description.Value.Length, 100, nameof(command.Description));
Preconditions.AtLeast(command.Description.Value.Length, 1, nameof(command.Description));
}

options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds(guildId: guildId);

return await SendJsonAsync<ApplicationCommand>("PATCH", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, bucket, options: options).ConfigureAwait(false);
}
public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);

var bucket = new BucketIds(guildId: guildId);

await SendAsync<ApplicationCommand>("DELETE", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", bucket, options: options).ConfigureAwait(false);
}

//Interaction Responses
public async Task CreateInteractionResponse(InteractionResponse response, ulong interactionId, string interactionToken, RequestOptions options = null)
{
if(response.Data.IsSpecified && response.Data.Value.Content.IsSpecified)
Preconditions.AtMost(response.Data.Value.Content.Value.Length, 2000, nameof(response.Data.Value.Content));

options = RequestOptions.CreateOrClone(options);

await SendJsonAsync("POST", () => $"interactions/{interactionId}/{interactionToken}/callback", response, new BucketIds(), options: options);
}
public async Task ModifyInteractionResponse(ModifyInteractionResponseParams args, string interactionToken, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);

await SendJsonAsync("POST", () => $"webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", args, new BucketIds(), options: options);
}
public async Task DeleteInteractionResponse(string interactionToken, RequestOptions options = null)
{
options = RequestOptions.CreateOrClone(options);

await SendAsync("DELETE", () => $"webhooks/{this.CurrentUserId}/{interactionToken}/messages/@original", new BucketIds(), options: options);
}

public async Task<Message> CreateInteractionFollowupMessage(CreateWebhookMessageParams args, string token, RequestOptions options = null)
{
if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0)
Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));

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));

options = RequestOptions.CreateOrClone(options);

return await SendJsonAsync<Message>("POST", () => $"webhooks/{CurrentUserId}/{token}?wait=true", args, new BucketIds(), options: options).ConfigureAwait(false);
}

public async Task<Message> ModifyInteractionFollowupMessage(CreateWebhookMessageParams 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));

options = RequestOptions.CreateOrClone(options);

return await SendJsonAsync<Message>("PATCH", () => $"webhooks/{CurrentUserId}/{token}/messages/{id}", args, new BucketIds(), options: options).ConfigureAwait(false);
}

public async Task DeleteInteractionFollowupMessage(ulong id, string token, RequestOptions options = null)
{
Preconditions.NotEqual(id, 0, nameof(id));

options = RequestOptions.CreateOrClone(options);

await SendAsync("DELETE", () => $"webhooks/{CurrentUserId}/{token}/messages/{id}", new BucketIds(), options: options).ConfigureAwait(false);
}

//Guilds
public async Task<Guild> GetGuildAsync(ulong guildId, bool withCounts, RequestOptions options = null)
{


+ 12
- 0
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -107,6 +107,18 @@ namespace Discord.Rest
=> ClientHelper.GetVoiceRegionAsync(this, id, options);
public Task<RestWebhook> GetWebhookAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetWebhookAsync(this, id, options);
public Task<RestGlobalCommand> CreateGlobalCommand(SlashCommandCreationProperties properties, RequestOptions options = null)
=> InteractionHelper.CreateGlobalCommand(this, properties, options);
public Task<RestGlobalCommand> CreateGlobalCommand(Action<SlashCommandCreationProperties> func, RequestOptions options = null)
=> InteractionHelper.CreateGlobalCommand(this, func, options);
public Task<RestGuildCommand> CreateGuildCommand(SlashCommandCreationProperties properties, ulong guildId, RequestOptions options = null)
=> InteractionHelper.CreateGuildCommand(this, guildId, properties, options);
public Task<RestGuildCommand> CreateGuildCommand(Action<SlashCommandCreationProperties> func, ulong guildId, RequestOptions options = null)
=> InteractionHelper.CreateGuildCommand(this, guildId, func, options);
public Task<IReadOnlyCollection<RestGlobalCommand>> GetGlobalApplicationCommands(RequestOptions options = null)
=> ClientHelper.GetGlobalApplicationCommands(this, options);
public Task<IReadOnlyCollection<RestGuildCommand>> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null)
=> ClientHelper.GetGuildApplicationCommands(this, guildId, options);

//IDiscordClient
/// <inheritdoc />


+ 162
- 0
src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs View File

@@ -0,0 +1,162 @@
using Discord.API;
using Discord.API.Rest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Rest
{
internal static class InteractionHelper
{
internal static async Task<RestUserMessage> 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);
return entity;
}
// Global commands
internal static async Task<RestGlobalCommand> CreateGlobalCommand(BaseDiscordClient client,
Action<SlashCommandCreationProperties> func, RequestOptions options = null)
{
var args = new SlashCommandCreationProperties();
func(args);
return await CreateGlobalCommand(client, args, options).ConfigureAwait(false);
}
internal static async Task<RestGlobalCommand> CreateGlobalCommand(BaseDiscordClient client,
SlashCommandCreationProperties args, RequestOptions options = null)
{
if (args.Options.IsSpecified)
{
if (args.Options.Value.Count > 10)
throw new ArgumentException("Option count must be 10 or less");
}


var model = new CreateApplicationCommandParams()
{
Name = args.Name,
Description = args.Description,
Options = args.Options.IsSpecified
? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray()
: Optional<Discord.API.ApplicationCommandOption[]>.Unspecified
};

var cmd = await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false);
return RestGlobalCommand.Create(client, cmd);
}
internal static async Task<RestGlobalCommand> ModifyGlobalCommand(BaseDiscordClient client, RestGlobalCommand command,
Action<ApplicationCommandProperties> func, RequestOptions options = null)
{
ApplicationCommandProperties args = new ApplicationCommandProperties();
func(args);

if (args.Options.IsSpecified)
{
if (args.Options.Value.Count > 10)
throw new ArgumentException("Option count must be 10 or less");
}

var model = new Discord.API.Rest.ModifyApplicationCommandParams()
{
Name = args.Name,
Description = args.Description,
Options = args.Options.IsSpecified
? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray()
: Optional<Discord.API.ApplicationCommandOption[]>.Unspecified
};

var msg = await client.ApiClient.ModifyGlobalApplicationCommandAsync(model, command.Id, options).ConfigureAwait(false);
command.Update(msg);
return command;
}


internal static async Task DeleteGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, RequestOptions options = null)
{
Preconditions.NotNull(command, nameof(command));
Preconditions.NotEqual(command.Id, 0, nameof(command.Id));

await client.ApiClient.DeleteGlobalApplicationCommandAsync(command.Id, options).ConfigureAwait(false);
}

// Guild Commands
internal static async Task<RestGuildCommand> CreateGuildCommand(BaseDiscordClient client, ulong guildId,
Action<SlashCommandCreationProperties> func, RequestOptions options = null)
{
var args = new SlashCommandCreationProperties();
func(args);

return await CreateGuildCommand(client, guildId, args, options).ConfigureAwait(false);
}
internal static async Task<RestGuildCommand> CreateGuildCommand(BaseDiscordClient client, ulong guildId,
SlashCommandCreationProperties args, RequestOptions options = null)
{
Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name));
Preconditions.NotNullOrEmpty(args.Description, nameof(args.Description));


if (args.Options.IsSpecified)
{
if (args.Options.Value.Count > 10)
throw new ArgumentException("Option count must be 10 or less");

foreach(var item in args.Options.Value)
{
Preconditions.NotNullOrEmpty(item.Name, nameof(item.Name));
Preconditions.NotNullOrEmpty(item.Description, nameof(item.Description));
}
}

var model = new CreateApplicationCommandParams()
{
Name = args.Name,
Description = args.Description,
Options = args.Options.IsSpecified
? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray()
: Optional<Discord.API.ApplicationCommandOption[]>.Unspecified
};

var cmd = await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false);
return RestGuildCommand.Create(client, cmd, guildId);
}
internal static async Task<RestGuildCommand> ModifyGuildCommand(BaseDiscordClient client, RestGuildCommand command,
Action<ApplicationCommandProperties> func, RequestOptions options = null)
{
ApplicationCommandProperties args = new ApplicationCommandProperties();
func(args);

if (args.Options.IsSpecified)
{
if (args.Options.Value.Count > 10)
throw new ArgumentException("Option count must be 10 or less");
}

var model = new Discord.API.Rest.ModifyApplicationCommandParams()
{
Name = args.Name,
Description = args.Description,
Options = args.Options.IsSpecified
? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray()
: Optional<Discord.API.ApplicationCommandOption[]>.Unspecified
};

var msg = await client.ApiClient.ModifyGuildApplicationCommandAsync(model, command.GuildId, command.Id, options).ConfigureAwait(false);
command.Update(msg);
return command;
}

internal static async Task DeleteGuildCommand(BaseDiscordClient client, RestGuildCommand command, RequestOptions options = null)
{
Preconditions.NotNull(command, nameof(command));
Preconditions.NotEqual(command.Id, 0, nameof(command.Id));

await client.ApiClient.DeleteGuildApplicationCommandAsync(command.GuildId, command.Id, options).ConfigureAwait(false);
}
}
}

+ 61
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommand;

namespace Discord.Rest
{
/// <summary>
/// Represents a rest implementation of the <see cref="IApplicationCommand"/>
/// </summary>
public abstract class RestApplicationCommand : RestEntity<ulong>, IApplicationCommand
{
public ulong ApplicationId { get; private set; }

public string Name { get; private set; }

public string Description { get; private set; }

public IReadOnlyCollection<RestApplicationCommandOption> Options { get; private set; }

public RestApplicationCommandType CommandType { get; internal set; }

public DateTimeOffset CreatedAt
=> SnowflakeUtils.FromSnowflake(this.Id);

internal RestApplicationCommand(BaseDiscordClient client, ulong id)
: base(client, id)
{

}

internal static RestApplicationCommand Create(BaseDiscordClient client, Model model, RestApplicationCommandType type, ulong guildId = 0)
{
if (type == RestApplicationCommandType.GlobalCommand)
return RestGlobalCommand.Create(client, model);

if (type == RestApplicationCommandType.GuildCommand)
return RestGuildCommand.Create(client, model, guildId);

return null;
}

internal virtual void Update(Model model)
{
this.ApplicationId = model.ApplicationId;
this.Name = model.Name;
this.Description = model.Description;

this.Options = model.Options.IsSpecified
? model.Options.Value.Select(x => RestApplicationCommandOption.Create(x)).ToImmutableArray()
: null;
}

IReadOnlyCollection<IApplicationCommandOption> IApplicationCommand.Options => Options;

public virtual Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException();
}
}

+ 22
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandChoice.cs View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommandOptionChoice;

namespace Discord.Rest
{
public class RestApplicationCommandChoice : IApplicationCommandOptionChoice
{
public string Name { get; }

public object Value { get; }

internal RestApplicationCommandChoice(Model model)
{
this.Name = model.Name;
this.Value = model.Value;
}
}
}

+ 60
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommandOption;

namespace Discord.Rest
{
public class RestApplicationCommandOption : IApplicationCommandOption
{
public ApplicationCommandOptionType Type { get; private set; }

public string Name { get; private set; }

public string Description { get; private set; }

public bool? Default { get; private set; }

public bool? Required { get; private set; }

public IReadOnlyCollection<RestApplicationCommandChoice> Choices { get; private set; }

public IReadOnlyCollection<RestApplicationCommandOption> Options { get; private set; }

internal RestApplicationCommandOption() { }

internal static RestApplicationCommandOption Create(Model model)
{
var options = new RestApplicationCommandOption();
options.Update(model);
return options;
}

internal void Update(Model model)
{
this.Type = model.Type;
this.Name = model.Name;
this.Description = model.Description;

if (model.Default.IsSpecified)
this.Default = model.Default.Value;

if (model.Required.IsSpecified)
this.Required = model.Required.Value;

this.Options = model.Options.IsSpecified
? model.Options.Value.Select(x => Create(x)).ToImmutableArray()
: null;

this.Choices = model.Choices.IsSpecified
? model.Choices.Value.Select(x => new RestApplicationCommandChoice(x)).ToImmutableArray()
: null;
}

IReadOnlyCollection<IApplicationCommandOption> IApplicationCommandOption.Options => Options;
IReadOnlyCollection<IApplicationCommandOptionChoice> IApplicationCommandOption.Choices => Choices;
}
}

+ 14
- 0
src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Rest
{
public enum RestApplicationCommandType
{
GlobalCommand,
GuildCommand
}
}

+ 41
- 0
src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommand;

namespace Discord.Rest
{
/// <summary>
/// Represents a global Slash command
/// </summary>
public class RestGlobalCommand : RestApplicationCommand
{
internal RestGlobalCommand(BaseDiscordClient client, ulong id)
: base(client, id)
{
this.CommandType = RestApplicationCommandType.GlobalCommand;
}

internal static RestGlobalCommand Create(BaseDiscordClient client, Model model)
{
var entity = new RestGlobalCommand(client, model.Id);
entity.Update(model);
return entity;
}
public override async Task DeleteAsync(RequestOptions options = null)
=> await InteractionHelper.DeleteGlobalCommand(Discord, this).ConfigureAwait(false);

/// <summary>
/// Modifies this <see cref="RestApplicationCommand"/>.
/// </summary>
/// <param name="func">The delegate containing the properties to modify the command with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// The modified command
/// </returns>
public async Task<RestGlobalCommand> ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null)
=> await InteractionHelper.ModifyGlobalCommand(Discord, this, func, options).ConfigureAwait(false);
}
}

+ 41
- 0
src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommand;

namespace Discord.Rest
{
public class RestGuildCommand : RestApplicationCommand
{
public ulong GuildId { get; set; }
internal RestGuildCommand(BaseDiscordClient client, ulong id, ulong guildId)
: base(client, id)
{
this.CommandType = RestApplicationCommandType.GuildCommand;
this.GuildId = guildId;
}

internal static RestGuildCommand Create(BaseDiscordClient client, Model model, ulong guildId)
{
var entity = new RestGuildCommand(client, model.Id, guildId);
entity.Update(model);
return entity;
}

public override async Task DeleteAsync(RequestOptions options = null)
=> await InteractionHelper.DeleteGuildCommand(Discord, this).ConfigureAwait(false);

/// <summary>
/// Modifies this <see cref="RestApplicationCommand"/>.
/// </summary>
/// <param name="func">The delegate containing the properties to modify the command with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// The modified command
/// </returns>
public async Task<RestGuildCommand> ModifyAsync(Action<ApplicationCommandProperties> func, RequestOptions options = null)
=> await InteractionHelper.ModifyGuildCommand(Discord, this, func, options).ConfigureAwait(false);
}
}

+ 30
- 0
src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs View File

@@ -0,0 +1,30 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API.Gateway
{
internal class ApplicationCommandCreatedUpdatedEvent
{
[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("id")]
public ulong Id { get; set; }

[JsonProperty("description")]
public string Description { get; set; }

[JsonProperty("application_id")]
public ulong ApplicationId { get; set; }

[JsonProperty("guild_id")]
public ulong GuildId { get; set; }

[JsonProperty("options")]
public List<Discord.API.ApplicationCommandOption> Options { get; set; }
}
}

+ 37
- 0
src/Discord.Net.WebSocket/API/Gateway/InteractionCreated.cs View File

@@ -0,0 +1,37 @@
using Discord.API;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.API.Gateway
{
internal class InteractionCreated
{
[JsonProperty("id")]
public ulong Id { get; set; }

[JsonProperty("type")]
public InteractionType Type { get; set; }

[JsonProperty("data")]
public Optional<ApplicationCommandInteractionData> Data { get; set; }

[JsonProperty("guild_id")]
public ulong GuildId { get; set; }

[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }

[JsonProperty("member")]
public GuildMember Member { get; set; }

[JsonProperty("token")]
public string Token { get; set; }

[JsonProperty("version")]
public int Version { get; set; }
}
}

+ 146
- 32
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -23,7 +23,7 @@ namespace Discord.WebSocket
/// <code language="cs" region="ChannelCreated"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, Task> ChannelCreated
public event Func<SocketChannel, Task> ChannelCreated
{
add { _channelCreatedEvent.Add(value); }
remove { _channelCreatedEvent.Remove(value); }
@@ -45,7 +45,8 @@ namespace Discord.WebSocket
/// <code language="cs" region="ChannelDestroyed"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, Task> ChannelDestroyed {
public event Func<SocketChannel, Task> ChannelDestroyed
{
add { _channelDestroyedEvent.Add(value); }
remove { _channelDestroyedEvent.Remove(value); }
}
@@ -67,10 +68,11 @@ namespace Discord.WebSocket
/// <code language="cs" region="ChannelUpdated"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketChannel, SocketChannel, Task> ChannelUpdated {
public event Func<SocketChannel, SocketChannel, Task> ChannelUpdated
{
add { _channelUpdatedEvent.Add(value); }
remove { _channelUpdatedEvent.Remove(value); }
}
}
internal readonly AsyncEvent<Func<SocketChannel, SocketChannel, Task>> _channelUpdatedEvent = new AsyncEvent<Func<SocketChannel, SocketChannel, Task>>();

//Messages
@@ -92,7 +94,8 @@ namespace Discord.WebSocket
/// <code language="cs" region="MessageReceived"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<SocketMessage, Task> MessageReceived {
public event Func<SocketMessage, Task> MessageReceived
{
add { _messageReceivedEvent.Add(value); }
remove { _messageReceivedEvent.Remove(value); }
}
@@ -124,7 +127,8 @@ namespace Discord.WebSocket
/// <code language="cs" region="MessageDeleted"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs" />
/// </example>
public event Func<Cacheable<IMessage, ulong>, ISocketMessageChannel, Task> MessageDeleted {
public event Func<Cacheable<IMessage, ulong>, ISocketMessageChannel, Task> MessageDeleted
{
add { _messageDeletedEvent.Add(value); }
remove { _messageDeletedEvent.Remove(value); }
}
@@ -182,7 +186,8 @@ namespace Discord.WebSocket
/// <see cref="ISocketMessageChannel"/> parameter.
/// </para>
/// </remarks>
public event Func<Cacheable<IMessage, ulong>, SocketMessage, ISocketMessageChannel, Task> MessageUpdated {
public event Func<Cacheable<IMessage, ulong>, SocketMessage, ISocketMessageChannel, Task> MessageUpdated
{
add { _messageUpdatedEvent.Add(value); }
remove { _messageUpdatedEvent.Remove(value); }
}
@@ -217,19 +222,22 @@ namespace Discord.WebSocket
/// <code language="cs" region="ReactionAdded"
/// source="..\Discord.Net.Examples\WebSocket\BaseSocketClient.Events.Examples.cs"/>
/// </example>
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task> ReactionAdded {
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task> ReactionAdded
{
add { _reactionAddedEvent.Add(value); }
remove { _reactionAddedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task>>();
/// <summary> Fired when a reaction is removed from a message. </summary>
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved {
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved
{
add { _reactionRemovedEvent.Add(value); }
remove { _reactionRemovedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent<Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, SocketReaction, Task>>();
/// <summary> Fired when all reactions to a message are cleared. </summary>
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, Task> ReactionsCleared {
public event Func<Cacheable<IUserMessage, ulong>, ISocketMessageChannel, Task> ReactionsCleared
{
add { _reactionsClearedEvent.Add(value); }
remove { _reactionsClearedEvent.Remove(value); }
}
@@ -259,19 +267,22 @@ namespace Discord.WebSocket

//Roles
/// <summary> Fired when a role is created. </summary>
public event Func<SocketRole, Task> RoleCreated {
public event Func<SocketRole, Task> RoleCreated
{
add { _roleCreatedEvent.Add(value); }
remove { _roleCreatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketRole, Task>> _roleCreatedEvent = new AsyncEvent<Func<SocketRole, Task>>();
/// <summary> Fired when a role is deleted. </summary>
public event Func<SocketRole, Task> RoleDeleted {
public event Func<SocketRole, Task> RoleDeleted
{
add { _roleDeletedEvent.Add(value); }
remove { _roleDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketRole, Task>> _roleDeletedEvent = new AsyncEvent<Func<SocketRole, Task>>();
/// <summary> Fired when a role is updated. </summary>
public event Func<SocketRole, SocketRole, Task> RoleUpdated {
public event Func<SocketRole, SocketRole, Task> RoleUpdated
{
add { _roleUpdatedEvent.Add(value); }
remove { _roleUpdatedEvent.Remove(value); }
}
@@ -279,37 +290,43 @@ namespace Discord.WebSocket

//Guilds
/// <summary> Fired when the connected account joins a guild. </summary>
public event Func<SocketGuild, Task> JoinedGuild {
public event Func<SocketGuild, Task> JoinedGuild
{
add { _joinedGuildEvent.Add(value); }
remove { _joinedGuildEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _joinedGuildEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when the connected account leaves a guild. </summary>
public event Func<SocketGuild, Task> LeftGuild {
public event Func<SocketGuild, Task> LeftGuild
{
add { _leftGuildEvent.Add(value); }
remove { _leftGuildEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _leftGuildEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when a guild becomes available. </summary>
public event Func<SocketGuild, Task> GuildAvailable {
public event Func<SocketGuild, Task> GuildAvailable
{
add { _guildAvailableEvent.Add(value); }
remove { _guildAvailableEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _guildAvailableEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when a guild becomes unavailable. </summary>
public event Func<SocketGuild, Task> GuildUnavailable {
public event Func<SocketGuild, Task> GuildUnavailable
{
add { _guildUnavailableEvent.Add(value); }
remove { _guildUnavailableEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _guildUnavailableEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when offline guild members are downloaded. </summary>
public event Func<SocketGuild, Task> GuildMembersDownloaded {
public event Func<SocketGuild, Task> GuildMembersDownloaded
{
add { _guildMembersDownloadedEvent.Add(value); }
remove { _guildMembersDownloadedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuild, Task>> _guildMembersDownloadedEvent = new AsyncEvent<Func<SocketGuild, Task>>();
/// <summary> Fired when a guild is updated. </summary>
public event Func<SocketGuild, SocketGuild, Task> GuildUpdated {
public event Func<SocketGuild, SocketGuild, Task> GuildUpdated
{
add { _guildUpdatedEvent.Add(value); }
remove { _guildUpdatedEvent.Remove(value); }
}
@@ -317,43 +334,50 @@ namespace Discord.WebSocket

//Users
/// <summary> Fired when a user joins a guild. </summary>
public event Func<SocketGuildUser, Task> UserJoined {
public event Func<SocketGuildUser, Task> UserJoined
{
add { _userJoinedEvent.Add(value); }
remove { _userJoinedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildUser, Task>> _userJoinedEvent = new AsyncEvent<Func<SocketGuildUser, Task>>();
/// <summary> Fired when a user leaves a guild. </summary>
public event Func<SocketGuildUser, Task> UserLeft {
public event Func<SocketGuildUser, Task> UserLeft
{
add { _userLeftEvent.Add(value); }
remove { _userLeftEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildUser, Task>> _userLeftEvent = new AsyncEvent<Func<SocketGuildUser, Task>>();
/// <summary> Fired when a user is banned from a guild. </summary>
public event Func<SocketUser, SocketGuild, Task> UserBanned {
public event Func<SocketUser, SocketGuild, Task> UserBanned
{
add { _userBannedEvent.Add(value); }
remove { _userBannedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, SocketGuild, Task>> _userBannedEvent = new AsyncEvent<Func<SocketUser, SocketGuild, Task>>();
/// <summary> Fired when a user is unbanned from a guild. </summary>
public event Func<SocketUser, SocketGuild, Task> UserUnbanned {
public event Func<SocketUser, SocketGuild, Task> UserUnbanned
{
add { _userUnbannedEvent.Add(value); }
remove { _userUnbannedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, SocketGuild, Task>> _userUnbannedEvent = new AsyncEvent<Func<SocketUser, SocketGuild, Task>>();
/// <summary> Fired when a user is updated. </summary>
public event Func<SocketUser, SocketUser, Task> UserUpdated {
public event Func<SocketUser, SocketUser, Task> UserUpdated
{
add { _userUpdatedEvent.Add(value); }
remove { _userUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, SocketUser, Task>> _userUpdatedEvent = new AsyncEvent<Func<SocketUser, SocketUser, Task>>();
/// <summary> Fired when a guild member is updated, or a member presence is updated. </summary>
public event Func<SocketGuildUser, SocketGuildUser, Task> GuildMemberUpdated {
public event Func<SocketGuildUser, SocketGuildUser, Task> GuildMemberUpdated
{
add { _guildMemberUpdatedEvent.Add(value); }
remove { _guildMemberUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildUser, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent<Func<SocketGuildUser, SocketGuildUser, Task>>();
internal readonly AsyncEvent<Func<SocketGuildUser, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent<Func<SocketGuildUser, SocketGuildUser, Task>>();
/// <summary> Fired when a user joins, leaves, or moves voice channels. </summary>
public event Func<SocketUser, SocketVoiceState, SocketVoiceState, Task> UserVoiceStateUpdated {
public event Func<SocketUser, SocketVoiceState, SocketVoiceState, Task> UserVoiceStateUpdated
{
add { _userVoiceStateUpdatedEvent.Add(value); }
remove { _userVoiceStateUpdatedEvent.Remove(value); }
}
@@ -361,30 +385,34 @@ namespace Discord.WebSocket
/// <summary> Fired when the bot connects to a Discord voice server. </summary>
public event Func<SocketVoiceServer, Task> VoiceServerUpdated
{
add { _voiceServerUpdatedEvent.Add(value); }
add { _voiceServerUpdatedEvent.Add(value); }
remove { _voiceServerUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketVoiceServer, Task>> _voiceServerUpdatedEvent = new AsyncEvent<Func<SocketVoiceServer, Task>>();
/// <summary> Fired when the connected account is updated. </summary>
public event Func<SocketSelfUser, SocketSelfUser, Task> CurrentUserUpdated {
public event Func<SocketSelfUser, SocketSelfUser, Task> CurrentUserUpdated
{
add { _selfUpdatedEvent.Add(value); }
remove { _selfUpdatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketSelfUser, SocketSelfUser, Task>> _selfUpdatedEvent = new AsyncEvent<Func<SocketSelfUser, SocketSelfUser, Task>>();
/// <summary> Fired when a user starts typing. </summary>
public event Func<SocketUser, ISocketMessageChannel, Task> UserIsTyping {
public event Func<SocketUser, ISocketMessageChannel, Task> UserIsTyping
{
add { _userIsTypingEvent.Add(value); }
remove { _userIsTypingEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketUser, ISocketMessageChannel, Task>> _userIsTypingEvent = new AsyncEvent<Func<SocketUser, ISocketMessageChannel, Task>>();
/// <summary> Fired when a user joins a group channel. </summary>
public event Func<SocketGroupUser, Task> RecipientAdded {
public event Func<SocketGroupUser, Task> RecipientAdded
{
add { _recipientAddedEvent.Add(value); }
remove { _recipientAddedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGroupUser, Task>> _recipientAddedEvent = new AsyncEvent<Func<SocketGroupUser, Task>>();
/// <summary> Fired when a user is removed from a group channel. </summary>
public event Func<SocketGroupUser, Task> RecipientRemoved {
public event Func<SocketGroupUser, Task> RecipientRemoved
{
add { _recipientRemovedEvent.Add(value); }
remove { _recipientRemovedEvent.Remove(value); }
}
@@ -431,5 +459,91 @@ namespace Discord.WebSocket
remove { _inviteDeletedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketGuildChannel, string, Task>> _inviteDeletedEvent = new AsyncEvent<Func<SocketGuildChannel, string, Task>>();

//Interactions
/// <summary>
/// Fired when an Interaction is created.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an interaction is created. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketInteraction"/> as its parameter.
/// </para>
/// <para>
/// The interaction created will be passed into the <see cref="SocketInteraction"/> parameter.
/// </para>
/// </remarks>
public event Func<SocketInteraction, Task> InteractionCreated
{
add { _interactionCreatedEvent.Add(value); }
remove { _interactionCreatedEvent.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketInteraction, Task>> _interactionCreatedEvent = new AsyncEvent<Func<SocketInteraction, Task>>();

/// <summary>
/// Fired when a guild application command is created.
///</summary>
///<remarks>
/// <para>
/// This event is fired when an application command is created. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketApplicationCommand"/> as its parameter.
/// </para>
/// <para>
/// The command that was deleted will be passed into the <see cref="SocketApplicationCommand"/> parameter.
/// </para>
/// <note>
/// <b>This event is an undocumented discord event and may break at any time, its not recommended to rely on this event</b>
/// </note>
/// </remarks>
public event Func<SocketApplicationCommand, Task> ApplicationCommandCreated
{
add { _applicationCommandCreated.Add(value); }
remove { _applicationCommandCreated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketApplicationCommand, Task>> _applicationCommandCreated = new AsyncEvent<Func<SocketApplicationCommand, Task>>();

/// <summary>
/// Fired when a guild application command is updated.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an application command is updated. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketApplicationCommand"/> as its parameter.
/// </para>
/// <para>
/// The command that was deleted will be passed into the <see cref="SocketApplicationCommand"/> parameter.
/// </para>
/// <note>
/// <b>This event is an undocumented discord event and may break at any time, its not recommended to rely on this event</b>
/// </note>
/// </remarks>
public event Func<SocketApplicationCommand, Task> ApplicationCommandUpdated
{
add { _applicationCommandUpdated.Add(value); }
remove { _applicationCommandUpdated.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketApplicationCommand, Task>> _applicationCommandUpdated = new AsyncEvent<Func<SocketApplicationCommand, Task>>();

/// <summary>
/// Fired when a guild application command is deleted.
/// </summary>
/// <remarks>
/// <para>
/// This event is fired when an application command is deleted. The event handler must return a
/// <see cref="Task"/> and accept a <see cref="SocketApplicationCommand"/> as its parameter.
/// </para>
/// <para>
/// The command that was deleted will be passed into the <see cref="SocketApplicationCommand"/> parameter.
/// </para>
/// <note>
/// <b>This event is an undocumented discord event and may break at any time, its not recommended to rely on this event</b>
/// </note>
/// </remarks>
public event Func<SocketApplicationCommand, Task> ApplicationCommandDeleted
{
add { _applicationCommandDeleted.Add(value); }
remove { _applicationCommandDeleted.Remove(value); }
}
internal readonly AsyncEvent<Func<SocketApplicationCommand, Task>> _applicationCommandDeleted = new AsyncEvent<Func<SocketApplicationCommand, Task>>();
}
}

+ 1
- 1
src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<Import Project="../../StyleAnalyzer.targets"/>
<Import Project="../../StyleAnalyzer.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.WebSocket</AssemblyName>
<RootNamespace>Discord.WebSocket</RootNamespace>


+ 2
- 0
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -392,6 +392,8 @@ namespace Discord.WebSocket

client.InviteCreated += (invite) => _inviteCreatedEvent.InvokeAsync(invite);
client.InviteDeleted += (channel, invite) => _inviteDeletedEvent.InvokeAsync(channel, invite);

client.InteractionCreated += (interaction) => _interactionCreatedEvent.InvokeAsync(interaction);
}

//IDiscordClient


+ 84
- 1
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -73,6 +73,7 @@ namespace Discord.WebSocket
internal bool AlwaysDownloadUsers { get; private set; }
internal int? HandlerTimeout { get; private set; }
internal bool? ExclusiveBulkDelete { get; private set; }
internal bool AlwaysAcknowledgeInteractions { get; private set; }

internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;
/// <inheritdoc />
@@ -135,6 +136,7 @@ namespace Discord.WebSocket
UdpSocketProvider = config.UdpSocketProvider;
WebSocketProvider = config.WebSocketProvider;
AlwaysDownloadUsers = config.AlwaysDownloadUsers;
AlwaysAcknowledgeInteractions = config.AlwaysAcknowledgeInteractions;
HandlerTimeout = config.HandlerTimeout;
ExclusiveBulkDelete = config.ExclusiveBulkDelete;
State = new ClientState(0, 0);
@@ -632,7 +634,7 @@ namespace Discord.WebSocket
}
else if (_connection.CancelToken.IsCancellationRequested)
return;
if (BaseConfig.AlwaysDownloadUsers)
_ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers));

@@ -1799,7 +1801,88 @@ namespace Discord.WebSocket
}
}
break;
case "INTERACTION_CREATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.InteractionCreated>(_serializer);
if (State.GetChannel(data.ChannelId) is SocketGuildChannel channel)
{
var guild = channel.Guild;
if (!guild.IsSynced)
{
await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false);
return;
}

var interaction = SocketInteraction.Create(this, data);

if (this.AlwaysAcknowledgeInteractions)
await interaction.AcknowledgeAsync().ConfigureAwait(false);

await TimedInvokeAsync(_interactionCreatedEvent, nameof(InteractionCreated), interaction).ConfigureAwait(false);
}
else
{
await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false);
return;
}
}
break;
case "APPLICATION_COMMAND_CREATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.ApplicationCommandCreatedUpdatedEvent>(_serializer);

var guild = State.GetGuild(data.GuildId);
if(guild == null)
{
await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false);
return;
}

var applicationCommand = SocketApplicationCommand.Create(this, data);

await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false);
}
break;
case "APPLICATION_COMMAND_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.ApplicationCommandCreatedUpdatedEvent>(_serializer);

var guild = State.GetGuild(data.GuildId);
if (guild == null)
{
await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false);
return;
}

var applicationCommand = SocketApplicationCommand.Create(this, data);

await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false);
}
break;
case "APPLICATION_COMMAND_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false);

var data = (payload as JToken).ToObject<API.Gateway.ApplicationCommandCreatedUpdatedEvent>(_serializer);

var guild = State.GetGuild(data.GuildId);
if (guild == null)
{
await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false);
return;
}

var applicationCommand = SocketApplicationCommand.Create(this, data);

await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false);
}
break;
//Ignored (User only)
case "CHANNEL_PINS_ACK":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false);


+ 23
- 0
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -105,6 +105,29 @@ namespace Discord.WebSocket
/// </remarks>
public bool AlwaysDownloadUsers { get; set; } = false;

/// <summary>
/// Gets or sets whether or not interactions are acknowledge with source.
/// </summary>
/// <remarks>
/// <para>
/// Discord interactions will not appear in chat until the client responds to them. With this option set to
/// <see langword="true"/>, the client will automatically acknowledge the interaction with <see cref="InteractionResponseType.ACKWithSource"/>.
/// See <see href="https://discord.com/developers/docs/interactions/slash-commands#interaction-interactionresponsetype">the docs</see> on
/// responding to interactions for more info.
/// </para>
/// <para>
/// With this option set to <see langword="false"/>, you will have to acknowledge the interaction with
/// <see cref="SocketInteraction.RespondAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/>.
/// Only after the interaction is acknowledged, the origional slash command message will be visible.
/// </para>
/// <note>
/// Please note that manually acknowledging the interaction with a message reply will not provide any return data.
/// Automatically acknowledging the interaction without sending the message will allow for follow up responses to
/// be used; follow up responses return the message data sent.
/// </note>
/// </remarks>
public bool AlwaysAcknowledgeInteractions { get; set; } = true;

/// <summary>
/// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged.
/// Setting this property to <c>null</c>disables this check.


+ 12
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -1006,6 +1006,18 @@ namespace Discord.WebSocket
public Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null)
=> GuildHelper.GetWebhooksAsync(this, Discord, options);

//Interactions
/// <summary>
/// Gets this guilds slash commands commands
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of application commands found within the guild.
/// </returns>
public async Task<IReadOnlyCollection<RestApplicationCommand>> GetApplicationCommandsAsync(RequestOptions options = null)
=> await Discord.Rest.GetGuildApplicationCommands(this.Id, options);

//Emotes
/// <inheritdoc />
public Task<GuildEmote> GetEmoteAsync(ulong id, RequestOptions options = null)


+ 55
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommand.cs View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Gateway.ApplicationCommandCreatedUpdatedEvent;

namespace Discord.WebSocket
{
public class SocketApplicationCommand : SocketEntity<ulong>, IApplicationCommand
{
public ulong ApplicationId { get; private set; }

public string Name { get; private set; }

public string Description { get; private set; }

public IReadOnlyCollection<SocketApplicationCommandOption> Options { get; private set; }

public DateTimeOffset CreatedAt
=> SnowflakeUtils.FromSnowflake(this.Id);

public SocketGuild Guild
=> Discord.GetGuild(this.GuildId);
private ulong GuildId { get; set; }

internal SocketApplicationCommand(DiscordSocketClient client, ulong id)
: base(client, id)
{

}
internal static SocketApplicationCommand Create(DiscordSocketClient client, Model model)
{
var entity = new SocketApplicationCommand(client, model.Id);
entity.Update(model);
return entity;
}

internal void Update(Model model)
{
this.ApplicationId = model.ApplicationId;
this.Description = model.Description;
this.Name = model.Name;
this.GuildId = model.GuildId;

this.Options = model.Options.Any()
? model.Options.Select(x => SocketApplicationCommandOption.Create(x)).ToImmutableArray()
: new ImmutableArray<SocketApplicationCommandOption>();
}

public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException();
IReadOnlyCollection<IApplicationCommandOption> IApplicationCommand.Options => Options;
}
}

+ 32
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandChoice.cs View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommandOptionChoice;

namespace Discord.WebSocket
{
/// <summary>
/// Represents a choice for a <see cref="SocketApplicationCommandOption"/>
/// </summary>
public class SocketApplicationCommandChoice : IApplicationCommandOptionChoice
{
public string Name { get; private set; }

public object Value { get; private set; }

internal SocketApplicationCommandChoice() { }
internal static SocketApplicationCommandChoice Create(Model model)
{
var entity = new SocketApplicationCommandChoice();
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
this.Name = model.Name;
this.Value = model.Value;
}
}
}

+ 70
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketApplicationCommandOption.cs View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommandOption;

namespace Discord.WebSocket
{
/// <summary>
/// Represents an option for a <see cref="SocketApplicationCommand"/>
/// </summary>
public class SocketApplicationCommandOption : IApplicationCommandOption
{
public string Name { get; private set; }

public ApplicationCommandOptionType Type { get; private set; }

public string Description { get; private set; }

public bool? Default { get; private set; }

public bool? Required { get; private set; }

/// <summary>
/// Choices for string and int types for the user to pick from.
/// </summary>
public IReadOnlyCollection<SocketApplicationCommandChoice> Choices { get; private set; }

/// <summary>
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters.
/// </summary>
public IReadOnlyCollection<SocketApplicationCommandOption> Options { get; private set; }

internal SocketApplicationCommandOption() { }
internal static SocketApplicationCommandOption Create(Model model)
{
var entity = new SocketApplicationCommandOption();
entity.Update(model);
return entity;
}

internal void Update(Model model)
{
this.Name = model.Name;
this.Type = model.Type;
this.Description = model.Description;

this.Default = model.Default.IsSpecified
? model.Default.Value
: null;

this.Required = model.Required.IsSpecified
? model.Required.Value
: null;

this.Choices = model.Choices.IsSpecified
? model.Choices.Value.Select(x => SocketApplicationCommandChoice.Create(x)).ToImmutableArray()
: new ImmutableArray<SocketApplicationCommandChoice>();

this.Options = model.Options.IsSpecified
? model.Options.Value.Select(x => SocketApplicationCommandOption.Create(x)).ToImmutableArray()
: new ImmutableArray<SocketApplicationCommandOption>();
}

IReadOnlyCollection<IApplicationCommandOptionChoice> IApplicationCommandOption.Choices => Choices;
IReadOnlyCollection<IApplicationCommandOption> IApplicationCommandOption.Options => Options;
}
}

+ 215
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs View File

@@ -0,0 +1,215 @@
using Discord.Rest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.Gateway.InteractionCreated;

namespace Discord.WebSocket
{
/// <summary>
/// Represents an Interaction recieved over the gateway
/// </summary>
public class SocketInteraction : SocketEntity<ulong>, IDiscordInteraction
{
/// <summary>
/// The <see cref="SocketGuild"/> this interaction was used in
/// </summary>
public SocketGuild Guild
=> Discord.GetGuild(GuildId);

/// <summary>
/// The <see cref="SocketTextChannel"/> this interaction was used in
/// </summary>
public SocketTextChannel Channel
=> Guild.GetTextChannel(ChannelId);

/// <summary>
/// The <see cref="SocketGuildUser"/> who triggered this interaction
/// </summary>
public SocketGuildUser Member
=> Guild.GetUser(MemberId);

/// <summary>
/// The type of this interaction
/// </summary>
public InteractionType Type { get; private set; }

/// <summary>
/// The data associated with this interaction
/// </summary>
public SocketInteractionData Data { get; private set; }

/// <summary>
/// The token used to respond to this interaction
/// </summary>
public string Token { get; private set; }

/// <summary>
/// The version of this interaction
/// </summary>
public int Version { get; private set; }

public DateTimeOffset CreatedAt { get; }

/// <summary>
/// <see langword="true"/> if the token is valid for replying to, otherwise <see langword="false"/>
/// </summary>
public bool IsValidToken
=> CheckToken();

private ulong GuildId { get; set; }
private ulong ChannelId { get; set; }
private ulong MemberId { get; set; }

internal SocketInteraction(DiscordSocketClient client, ulong id)
: base(client, id)
{
}

internal static SocketInteraction Create(DiscordSocketClient client, Model model)
{
var entitiy = new SocketInteraction(client, model.Id);
entitiy.Update(model);
return entitiy;
}

internal void Update(Model model)
{
this.Data = model.Data.IsSpecified
? SocketInteractionData.Create(this.Discord, model.Data.Value)
: null;

this.GuildId = model.GuildId;
this.ChannelId = model.ChannelId;
this.Token = model.Token;
this.Version = model.Version;
this.MemberId = model.Member.User.Id;
this.Type = model.Type;
}
private bool CheckToken()
{
// Tokens last for 15 minutes according to https://discord.com/developers/docs/interactions/slash-commands#responding-to-an-interaction
return (DateTime.UtcNow - this.CreatedAt.UtcDateTime).TotalMinutes >= 15d;
}

/// <summary>
/// Responds to an Interaction.
/// <para>
/// If you have <see cref="DiscordSocketConfig.AlwaysAcknowledgeInteractions"/> set to <see langword="true"/>, You should use
/// <see cref="FollowupAsync(string, bool, Embed, InteractionResponseType, AllowedMentions, RequestOptions)"/> instead.
/// </para>
/// </summary>
/// <param name="text">The text of the message to be sent</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/></param>
/// <param name="embed">A <see cref="Embed"/> to send with this response</param>
/// <param name="Type">The type of response to this Interaction</param>
/// <param name="allowedMentions">The allowed mentions for this response</param>
/// <param name="options">The request options for this response</param>
/// <returns>
/// The <see cref="IMessage"/> sent as the response. If this is the first acknowledgement, it will return null;
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
/// <exception cref="InvalidOperationException">The parameters provided were invalid or the token was invalid</exception>

public async Task<IMessage> RespondAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, AllowedMentions allowedMentions = null, RequestOptions options = null)
{
if (Type == InteractionResponseType.Pong)
throw new InvalidOperationException($"Cannot use {Type} on a send message function");

if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");

if (Discord.AlwaysAcknowledgeInteractions)
return await FollowupAsync();

Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");

// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
{
if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) &&
allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0)
{
throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions));
}

if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) &&
allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0)
{
throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions));
}
}


var response = new API.InteractionResponse()
{
Type = Type,
Data = new API.InteractionApplicationCommandCallbackData(text)
{
AllowedMentions = allowedMentions?.ToModel(),
Embeds = embed != null
? new API.Embed[] { embed.ToModel() }
: Optional<API.Embed[]>.Unspecified,
TTS = isTTS
}
};

await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options);
return null;
}

/// <summary>
/// Sends a followup message for this interaction
/// </summary>
/// <param name="text">The text of the message to be sent</param>
/// <param name="isTTS"><see langword="true"/> if the message should be read out by a text-to-speech reader, otherwise <see langword="false"/></param>
/// <param name="embed">A <see cref="Embed"/> to send with this response</param>
/// <param name="Type">The type of response to this Interaction</param>
/// <param name="allowedMentions">The allowed mentions for this response</param>
/// <param name="options">The request options for this response</param>
/// <returns>
/// The sent message
/// </returns>
public async Task<IMessage> FollowupAsync(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource,
AllowedMentions allowedMentions = null, RequestOptions options = null)
{
if (Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.ACKWithSource || Type == InteractionResponseType.Pong)
throw new InvalidOperationException($"Cannot use {Type} on a send message function");

if (!IsValidToken)
throw new InvalidOperationException("Interaction token is no longer valid");

var args = new API.Rest.CreateWebhookMessageParams(text)
{
IsTTS = isTTS,
Embeds = embed != null
? new API.Embed[] { embed.ToModel() }
: Optional<API.Embed[]>.Unspecified,
};
return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options);
}

/// <summary>
/// Acknowledges this interaction with the <see cref="InteractionResponseType.ACKWithSource"/>
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation of acknowledging the interaction
/// </returns>
public async Task AcknowledgeAsync(RequestOptions options = null)
{
var response = new API.InteractionResponse()
{
Type = InteractionResponseType.ACKWithSource,
};

await Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, Token, options).ConfigureAwait(false);
}

IApplicationCommandInteractionData IDiscordInteraction.Data => Data;
}
}

+ 39
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionData.cs View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommandInteractionData;

namespace Discord.WebSocket
{
public class SocketInteractionData : SocketEntity<ulong>, IApplicationCommandInteractionData
{
public string Name { get; private set; }
public IReadOnlyCollection<SocketInteractionDataOption> Options { get; private set; }

internal SocketInteractionData(DiscordSocketClient client, ulong id)
: base(client, id)
{

}

internal static SocketInteractionData Create(DiscordSocketClient client, Model model)
{
var entity = new SocketInteractionData(client, model.Id);
entity.Update(model);
return entity;
}
internal void Update(Model model)
{
this.Name = model.Name;
this.Options = model.Options.IsSpecified
? model.Options.Value.Select(x => new SocketInteractionDataOption(x)).ToImmutableArray()
: null;

}

IReadOnlyCollection<IApplicationCommandInteractionDataOption> IApplicationCommandInteractionData.Options => Options;
}
}

+ 28
- 0
src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Model = Discord.API.ApplicationCommandInteractionDataOption;

namespace Discord.WebSocket
{
public class SocketInteractionDataOption : IApplicationCommandInteractionDataOption
{
public string Name { get; private set; }
public object? Value { get; private set; }

public IReadOnlyCollection<IApplicationCommandInteractionDataOption> Options { get; private set; }

internal SocketInteractionDataOption(Model model)
{
this.Name = Name;
this.Value = model.Value.IsSpecified ? model.Value.Value : null;

this.Options = model.Options.IsSpecified
? model.Options.Value.Select(x => new SocketInteractionDataOption(x)).ToImmutableArray()
: null;
}
}
}

Loading…
Cancel
Save