Table of Contents

Getting Started

The Interaction Service provides an attribute based framework for creating Discord Interaction handlers.

To start using the Interaction Service, you need to create a service instance. Optionally you can provide the InteractionService constructor with a InteractionServiceConfig to change the services behaviour to suit your needs.

...
// _client here is DiscordSocketClient.
// A different approach to passing in a restclient is also possible.
var _interactionService = new InteractionService(_client.Rest);

...

Modules

Attribute based Interaction handlers must be defined within a command module class. Command modules are responsible for executing the Interaction handlers and providing them with the necessary execution info and helper functions.

Command modules are transient objects. A new module instance is created before a command execution starts then it will be disposed right after the method returns.

Every module class must:

Optionally you can override the included :

  • OnModuleBuilding (executed after the module is built)
  • BeforeExecute (executed before a command execution starts)
  • AfterExecute (executed after a command execution concludes)

methods to configure the modules behaviour.

Every command module exposes a set of helper methods, namely:

  • RespondAsync() => Respond to the interaction
  • FollowupAsync() => Create a followup message for an interaction
  • ReplyAsync() => Send a message to the origin channel of the interaction
  • DeleteOriginalResponseAsync() => Delete the original interaction response

Commands

Valid Interaction Commands must comply with the following requirements:

return type max parameter count allowed parameter types attribute
Slash Command Task/Task<RuntimeResult> 25 any* [SlashCommand]
User Command Task/Task<RuntimeResult> 1 Implementations of IUser [UserCommand]
Message Command Task/Task<RuntimeResult> 1 Implementations of IMessage [MessageCommand]
Component Interaction Command Task/Task<RuntimeResult> inf string or string[] [ComponentInteraction]
Autocomplete Command Task/Task<RuntimeResult> - - [AutocompleteCommand]
Note

A TypeConverter that is capable of parsing type in question must be registered to the InteractionService instance. You should avoid using long running code in your command module. Depending on your setup, long running code may block the Gateway thread of your bot, interrupting its connection to Discord.

Slash Commands

Slash Commands are created using the SlashCommandAttribute. Every Slash Command must declare a name and a description. You can check Discords Application Command Naming Guidelines here.

[SlashCommand("echo", "Echo an input")]
public async Task Echo(string input)
{
    await RespondAsync(input);
}

Parameters

Slash Commands can have up to 25 method parameters. You must name your parameters in accordance with Discords Naming Guidelines. InteractionService also features a pascal casing seperator for formatting parameter names with pascal casing into Discord compliant parameter names('parameterName' => 'parameter-name'). By default, your methods can feature the following parameter types:

  • Implementations of IUser
  • Implementations of IChannel
  • Implementations of IRole
  • Implementations of IMentionable
  • Implementations of [IAttachment]
  • string
  • float, double, decimal
  • bool
  • char
  • sbyte, byte
  • int16, int32, int64
  • uint16, uint32, uint64
  • enum
  • DateTime
  • TimeSpan
Note

Enum values are registered as multiple choice options and are enforced by Discord. Use the [Hide] attribute on enum values to prevent them from getting registered.


You can use more specialized implementations of IChannel to restrict the allowed channel types for a channel type option.

interface Channel Type
IStageChannel Stage Channels
IVoiceChannel Voice Channels
IDMChannel DM Channels
IGroupChannel Group Channels
ICategoryChannel Category Channels
INewsChannel News Channels
IThreadChannel Public, Private, News Threads
ITextChannel Text Channels

Optional Parameters

Parameters with default values (ie. int count = 0) will be displayed as optional parameters on Discord Client.

Parameter Summary

By using the SummaryAttribute you can customize the displayed name and description of a parameter

[Summary(description: "this is a parameter description")] string input

Parameter Choices

ChoiceAttribute can be used to add choices to a parameter.

[SlashCommand("blep", "Send a random adorable animal photo")]
public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Guinea pig", "GuineaPig")] string animal)
{
    ...
}

// In most cases, you can use an enum to replace the separate choice attributes in a command.

public enum Animal
{
    Cat,
    Dog,
    // You can also use the ChoiceDisplay attribute to change how they appear in the choice menu.
    [ChoiceDisplay("Guinea pig")]
    GuineaPig
}

[SlashCommand("blep", "Send a random adorable animal photo")]
public async Task Blep(Animal animal)
{
    ...
}
```

This Slash Command will be displayed exactly the same as the previous example.

Channel Types

Channel types for an IChannel parameter can also be restricted using the ChannelTypesAttribute.

[SlashCommand("name", "Description")]
public async Task Command([ChannelTypes(ChannelType.Stage, ChannelType.Text)] IChannel channel)
{
    ...
}

In this case, user can only input Stage Channels and Text Channels to this parameter.

Min/Max Value

You can specify the permitted max/min value for a number type parameter using the MaxValueAttribute and MinValueAttribute.

Complex Parameters

This allows users to create slash command options using an object's constructor allowing complex objects to be created which cannot be infered from only one input value. Constructor methods support every attribute type that can be used with the regular slash commands ([Autocomplete], [Summary] etc. ). Preferred constructor of a Type can be specified either by passing a Type[] to the [ComplexParameterAttribute] or tagging a type constructor with the [ComplexParameterCtorAttribute]. If nothing is specified, the InteractionService defaults to the only public constructor of the type. TypeConverter pattern is used to parse the constructor methods objects.

public class Vector3
{
    public int X {get;}
    public int Y {get;}
    public int Z {get;}

    public Vector3()
    {
        X = 0;
        Y = 0;
        Z = 0;
    }

    [ComplexParameterCtor]
    public Vector3(int x, int y, int z)
    {
        X = x;
        Y = y;
        Z = z;
    }
}

// Both of the commands below are displayed to the users identically.

// With complex parameter
[SlashCommand("create-vector", "Create a 3D vector.")]
public async Task CreateVector([ComplexParameter]Vector3 vector3)
{
    ...
}

// Without complex parameter
[SlashCommand("create-vector", "Create a 3D vector.")]
public async Task CreateVector(int x, int y, int z)
{
    ...
}

Interaction service complex parameter constructors are prioritized in the following order:

  1. Constructor matching the signature provided in the [ComplexParameter(Type[])] overload.
  2. Constuctor tagged with [ComplexParameterCtor].
  3. Type's only public constuctor.

DM Permissions

Warning

EnabledInDmAttribute is being deprecated in favor of CommandContextTypes attribute.

You can use the EnabledInDmAttribute to configure whether a globally-scoped top level command should be enabled in Dms or not. Only works on top level commands.

Default Member Permissions

[DefaultMemberPermissionsAttribute] can be used when creating a command to set the permissions a user must have to use the command. Permission overwrites can be configured from the Integrations page of Guild Settings. [DefaultMemberPermissionsAttribute] cumulatively propagates down the class hierarchy until it reaches a top level command. This attribute can be only used on top level commands and will not work on commands that are nested in command groups.

User Commands

A valid User Command must have the following structure:

[UserCommand("Say Hello")]
public async Task SayHello(IUser user)
{
    ...
}
Warning

User commands can only have one parameter and its type must be an implementation of IUser.

Message Commands

A valid Message Command must have the following structure:

[MessageCommand("Bookmark")]
public async Task Bookmark(IMessage msg)
{
    ...
}
Warning

Message commands can only have one parameter and its type must be an implementation of IMessage.

Component Interaction Commands

Component Interaction Commands are used to handle interactions that originate from Discord Message Components. This pattern is particularly useful if you will be reusing a set a Custom IDs.

Component Interaction Commands support wild card matching, by default * character can be used to create a wild card pattern. Interaction Service will use lazy matching to capture the words corresponding to the wild card character. And the captured words will be passed on to the command method in the same order they were captured.

[ComponentInteraction("player:*,*")]
public async Task Play(string op, string name)
{
    ...
}

You may use as many wild card characters as you want.

Note

If Interaction Service receives a component interaction with player:play,rickroll custom id, op will be play and name will be rickroll

Select Menus

Unlike button interactions, select menu interactions also contain the values of the selected menu items. In this case, you should structure your method to accept a string array.

Note

Use arrays of IUser, IChannel, IRole, IMentionable or their implementations to get data from a select menu with respective type.

[ComponentInteraction("role_selection")]
public async Task RoleSelection(string[] selectedRoles)
{
    ...
}

[ComponentInteraction("role_selection_*")]
public async Task RoleSelection(string id, string[] selectedRoles)
{
    ...
}
Note

Wildcards may also be used to match a select menu ID, though keep in mind that the array containing the select menu values should be the last parameter.

Autocomplete Commands

Autocomplete commands must be parameterless methods. A valid Autocomplete command must have the following structure:

[AutocompleteCommand("parameter_name", "command_name")]
public async Task Autocomplete()
{
    string userInput = (Context.Interaction as SocketAutocompleteInteraction).Data.Current.Value.ToString();

    IEnumerable<AutocompleteResult> results = new[]
    {
        new AutocompleteResult("foo", "foo_value"),
        new AutocompleteResult("bar", "bar_value"),
        new AutocompleteResult("baz", "baz_value"),
    }.Where(x => x.Name.StartsWith(userInput, StringComparison.InvariantCultureIgnoreCase)); // only send suggestions that starts with user's input; use case insensitive matching


    // max - 25 suggestions at a time
    await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results.Take(25));
}

// you need to add `Autocomplete` attribute before parameter to add autocompletion to it
[SlashCommand("command_name", "command_description")]
public async Task ExampleCommand([Summary("parameter_name"), Autocomplete] string parameterWithAutocompletion)
    => await RespondAsync($"Your choice: {parameterWithAutocompletion}");

Alternatively, you can use the AutocompleteHandlers to simplify this workflow.

Modals

Modal commands last parameter must be an implementation of IModal. A Modal implementation would look like this:

// Registers a command that will respond with a modal.
[SlashCommand("food", "Tell us about your favorite food.")]
public async Task Command()
    => await Context.Interaction.RespondWithModalAsync<FoodModal>("food_menu");

// Defines the modal that will be sent.
public class FoodModal : IModal
{
    public string Title => "Fav Food";
    // Strings with the ModalTextInput attribute will automatically become components.
    [InputLabel("What??")]
    [ModalTextInput("food_name", placeholder: "Pizza", maxLength: 20)]
    public string Food { get; set; }

    // Additional paremeters can be specified to further customize the input.    
    // Parameters can be optional
    [RequiredInput(false)]
    [InputLabel("Why??")]
    [ModalTextInput("food_reason", TextInputStyle.Paragraph, "Kuz it's tasty", maxLength: 500)]
    public string Reason { get; set; }
}

// Responds to the modal.
[ModalInteraction("food_menu")]
public async Task ModalResponse(FoodModal modal)
{
    // Check if "Why??" field is populated
    string reason = string.IsNullOrWhiteSpace(modal.Reason)
        ? "."
        : $" because {modal.Reason}";

    // Build the message to send.
    string message = "hey @everyone, I just learned " +
        $"{Context.User.Mention}'s favorite food is " +
        $"{modal.Food}{reason}";

    // Specify the AllowedMentions so we don't actually ping everyone.
    AllowedMentions mentions = new();
    mentions.AllowedTypes = AllowedMentionTypes.Users;

    // Respond to the modal.
    await RespondAsync(message, allowedMentions: mentions, ephemeral: true);
}
Note

If you are using Modals in the interaction service it is highly recommended that you enable PreCompiledLambdas in your config to prevent performance issues.

Interaction Context

Every command module provides its commands with an execution context. This context property includes general information about the underlying interaction that triggered the command execution. The base command context.

You can design your modules to work with different implementation types of [IInteractionContext]. To achieve this, make sure your module classes inherit from the generic variant of the InteractionModuleBase.

Note

Context type must be consistent throughout the project, or you will run into issues during runtime.

The InteractionService ships with 4 different kinds of InteractionContext:

  1. InteractionContext]: A bare-bones execution context consisting of only implementation neutral interfaces
  2. SocketInteractionContext: An execution context for use with DiscordSocketClient. Socket entities are exposed in this context without the need of casting them.
  3. ShardedInteractionContext: [DiscordShardedClient] variant of the SocketInteractionContext
  4. RestInteractionContext: An execution context designed to be used with a DiscordRestClient and webhook based interactions pattern

You can create custom Interaction Contexts by implementing the [IInteractionContext] interface.

One problem with using the concrete type InteractionContexts is that you cannot access the information that is specific to different interaction types without casting. Concrete type interaction contexts are great for creating shared interaction modules but you can also use the generic variants of the built-in interaction contexts to create interaction specific interaction modules.

[!INFO] Message component interactions have access to a special method called UpdateAsync() to update the body of the method the interaction originated from. Normally this wouldn't be accessible without casting the Context.Interaction.

discordClient.ButtonExecuted += async (interaction) => 
{
    var ctx = new SocketInteractionContext<SocketMessageComponent>(discordClient, interaction);
    await _interactionService.ExecuteCommandAsync(ctx, serviceProvider);
};

public class MessageComponentModule : InteractionModuleBase<SocketInteractionContext<SocketMessageComponent>>
{
    [ComponentInteraction("custom_id")]
    public async Task Command()
    {
        await Context.Interaction.UpdateAsync(...);
    }
}

Loading Modules

InteractionService can automatically discover and load modules that inherit InteractionModuleBase from an Assembly. Call InteractionService.AddModulesAsync() to use this functionality.

Note

You can also manually add Interaction modules using the InteractionService.AddModuleAsync() method by providing the module type you want to load.

Resolving Module Dependencies

Module dependencies are resolved using the Constructor Injection and Property Injection patterns. Meaning, the constructor parameters and public settable properties of a module will be assigned using the IServiceProvider. For more information on dependency injection, read the DependencyInjection guides.

Note

On every command execution, if the 'AutoServiceScopes' option is enabled in the config , module dependencies are resolved using a new service scope which allows you to utilize scoped service instances, just like in Asp.Net. Including the precondition checks, every module method is executed using the same service scope and service scopes are disposed right after the AfterExecute method returns. This doesn't apply to methods other than ExecuteAsync().

Module Groups

Module groups allow you to create sub-commands and sub-commands groups. By nesting commands inside a module that is tagged with GroupAttribute you can create prefixed commands.

Warning

Although creating nested module structures are allowed, you are not permitted to use more than 2 GroupAttribute's in module hierarchy.

Note

To not use the command group's name as a prefix for component or modal interaction's custom id set ignoreGroupNames parameter to true in classes with GroupAttribute

However, you have to be careful to prevent overlapping ids of buttons and modals.

// You can put commands in groups
[Group("group-name", "Group description")]
public class CommandGroupModule : InteractionModuleBase<SocketInteractionContext>
{
    // This command will look like
    // group-name ping
    [SlashCommand("ping", "Get a pong")]
    public async Task PongSubcommand()
        => await RespondAsync("Pong!");
    
    // And even in sub-command groups
    [Group("subcommand-group-name", "Subcommand group description")]
    public class SubСommandGroupModule : InteractionModuleBase<SocketInteractionContext>
    {
        // This command will look like
        // group-name subcommand-group-name echo
        [SlashCommand("echo", "Echo an input")]
        public async Task EchoSubcommand(string input)
            => await RespondAsync(input, components: new ComponentBuilder().WithButton("Echo", $"echoButton_{input}").Build());

        // Component interaction with ignoreGroupNames set to true
        [ComponentInteraction("echoButton_*", true)]
        public async Task EchoButton(string input)
            => await RespondAsync(input);  
    }
}

Executing Commands

Any of the following socket events can be used to execute commands:

These events will trigger for the specific type of interaction they inherit their name from. The InteractionCreated event will trigger for all. An example of executing a command from an event can be seen here:

// Theres multiple ways to subscribe to the event, depending on your application. Please use the approach fit to your type of client.
// DiscordSocketClient:
_socketClient.InteractionCreated += async (x) =>
{
    var ctx = new SocketInteractionContext(_socketClient, x);
    await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider);
}

// DiscordShardedClient:
_shardedClient.InteractionCreated += async (x) =>
{
    var ctx = new ShardedInteractionContext(_shardedClient, x);
    await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider);
}

Commands can be either executed on the gateway thread or on a separate thread from the thread pool. This behaviour can be configured by changing the RunMode property of InteractionServiceConfig or by setting the runMode parameter of a command attribute.

Warning

In the example above, no form of post-execution is presented. Please carefully read the [Post Execution Documentation] for the best approach in resolving the result based on your RunMode.

You can also configure the way InteractionService executes the commands. By default, commands are executed using ConstructorInfo.Invoke() to create module instances and MethodInfo.Invoke() method for executing the method bodies. By setting, InteractionServiceConfig.UseCompiledLambda to true, you can make InteractionService create module instances and execute commands using Compiled Lambda expressions. This cuts down on command execution time but it might add some memory overhead.

Time it takes to create a module instance and execute a Task.Delay(0) method using the Reflection methods compared to Compiled Lambda expressions:

Method Mean Error StdDev
ReflectionInvoke 225.93 ns 4.522 ns 7.040 ns
CompiledLambda 48.79 ns 0.981 ns 1.276 ns

Registering Commands to Discord

Application commands loaded to the Interaction Service can be registered to Discord using a number of different methods. In most cases RegisterCommandsGloballyAsync() and RegisterCommandsToGuildAsync() are the methods to use. Command registration methods can only be used after the gateway client is ready or the rest client is logged in.

#if DEBUG
    await interactionService.RegisterCommandsToGuildAsync(<test_guild_id>);
#else
    await interactionService.RegisterCommandsGloballyAsync();
#endif

Methods like AddModulesToGuildAsync(), AddCommandsToGuildAsync(), AddModulesGloballyAsync() and AddCommandsGloballyAsync() can be used to register cherry picked modules or commands to global/guild scopes.

Note

DontAutoRegisterAttribute can be used on module classes to prevent RegisterCommandsGloballyAsync() and RegisterCommandsToGuildAsync() from registering them to the Discord.

Interaction Utility

Interaction Service ships with a static InteractionUtility class which contains some helper methods to asynchronously waiting for Discord Interactions. For instance, WaitForInteractionAsync() method allows you to wait for an Interaction for a given amount of time. This method returns the first encountered Interaction that satisfies the provided predicate.

Warning

If you are running the Interaction Service on RunMode.Sync you should avoid using this method in your commands, as it will block the gateway thread and interrupt your bots connection.

Webhook Based Interactions

Instead of using the gateway to receive Discord Interactions, Discord allows you to receive Interaction events over Webhooks. Interaction Service also supports this Interaction type but to be able to respond to the Interactions within your command modules you need to perform the following:

  • Make your modules inherit RestInteractionModuleBase
  • Set the ResponseCallback property of InteractionServiceConfig so that the ResponseCallback delegate can be used to create HTTP responses from a deserialized json object string.
  • Use the interaction endpoints of the module base instead of the interaction object (ie. RespondAsync(), FollowupAsync()...).

Localization

Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups (GroupAttribute), Slash Commands (SlashCommandAttribute), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an ILocalizationManager instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in ILocalizationManager implementations: ResxLocalizationManager and JsonLocalizationManager.

ResXLocalizationManager

ResxLocalizationManager uses . delimited key names to traverse the resource files and get the localized strings (group1.group2.command.parameter.name). A ResxLocalizationManager instance must be initialized with a base resource name, a target assembly and a collection of CultureInfos. Every key path must end with either .name or .description, including parameter choice strings. Discord.Tools.LocalizationTemplate.Resx dotnet tool can be used to create localization file templates.

JsonLocalizationManager

JsonLocalizationManager uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema here. JsonLocalizationManager accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to:

{
    "command_1":{
        "name": "localized_name",
        "description": "localized_description",
        "parameter_1":{
            "name": "localized_name",
            "description": "localized_description"
        }
    },
    "group_1":{
        "name": "localized_name",
        "description": "localized_description",
        "command_1":{
            "name": "localized_name",
             "description": "localized_description",
             "parameter_1":{
                 "name": "localized_name",
                  "description": "localized_description"
            },
            "parameter_2":{
                 "name": "localized_name",
                  "description": "localized_description"
            }
        }
    }
}

User Apps

User apps are the kind of Discord applications that are installed onto a user instead of a guild, thus making commands usable anywhere on Discord. Note that only users who have installed the application will see the commands. This sample shows you how to create a simple user install command.


// This parameteres can be configured on the module level
// Set supported command context types to Bot DMs and Private Channels (regular DM & GDM)
[CommandContextType(InteractionContextType.BotDm, InteractionContextType.PrivateChannel)]
// Set supported integration installation type to User Install
[IntegrationType(ApplicationIntegrationType.UserInstall)]
public class CommandModule() : InteractionModuleBase<SocketInteractionContext>
{
    [SlashCommand("test", "Just a test command")]
    public async Task TestCommand()
        => await RespondAsync("Hello There");

    // But can also be overridden on the command level
    [CommandContextType(InteractionContextType.BotDm, InteractionContextType.PrivateChannel, InteractionContextType.Guild)]
    [IntegrationType(ApplicationIntegrationType.GuildInstall)]
    [SlashCommand("echo", "Echo the input")]
    public async Task EchoCommand(string input)
        => await RespondAsync($"You said: {input}");
}