Remora.Discord.Interactivity 5.0.0

Prefix Reserved
dotnet add package Remora.Discord.Interactivity --version 5.0.0                
NuGet\Install-Package Remora.Discord.Interactivity -Version 5.0.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Remora.Discord.Interactivity" Version="5.0.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Remora.Discord.Interactivity --version 5.0.0                
#r "nuget: Remora.Discord.Interactivity, 5.0.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Remora.Discord.Interactivity as a Cake Addin
#addin nuget:?package=Remora.Discord.Interactivity&version=5.0.0

// Install Remora.Discord.Interactivity as a Cake Tool
#tool nuget:?package=Remora.Discord.Interactivity&version=5.0.0                

Remora.Discord.Interactivity

This package provides a framework for creating interaction-driven entities using Discord's message components.

Structure

The library's design is very similar to Remora.Discord.Commands, utilizing many of the same concepts. Interactions are treated as named commands, letting you separate the logic from the frontend in a clean and reusable way.

All the familiar concepts from normal commands are available to you such as conditions, parsers, groups, and true concurrency.

Usage

First, add the required services to the dependency injection container.

services.AddInteractivity();

In order to respond to incoming component interactions, declare a class that inherits from the abstract InteractionGroup class.

public class MyInteractions : InteractionGroup
{
}

Each supported component interaction has an associated attribute and function signature that lets the incoming data bind to and invoke your method. To use them, declare one or more methods and decorate them with the appropriate attribute inside your interaction group.

As with Remora.Commands, interaction methods may return any type implementing IResult (including IResult itself) as either a Task<T> or ValueTask<T>.

Buttons

Buttons are parameterless functions decorated with the Button attribute.

[Button("my-button")]
public Task<Result> OnButtonPressedAsync()
{
    // ...
}

Select Menus

Select menus are functions with a list of objects as its sole parameter, decorated with the SelectMenu attribute. There are multiple types of select menu, namely string, user, role, mentionable (users AND roles), and channel select menus.

When using a string select menu, the command parameter must be named values, and will contain zero or more values selected by the end user in the menu.

You can control the number of allowed values when creating the component through its MinValues and MaxValues properties.

[SelectMenu("my-menu")]
public Task<Result> OnMenuSelectionAsync(IReadOnlyList<string> values)
{
    // ...
}

Since this set of values are passed through Remora.Commands' parsing system, you can use any parsable type (including your own!) as the list's contained type. This means that, for example, the following snippets are all valid.

[SelectMenu("my-menu")]
public Task<Result> OnMenuSelectionAsync(IReadOnlyList<int> values)
{
    // ...
}
[SelectMenu("my-menu")]
public Task<Result> OnMenuSelectionAsync(IReadOnlyList<Snowflake> values)
{
    // ...
}
[SelectMenu("my-menu")]
public Task<Result> OnMenuSelectionAsync(IReadOnlyList<MyArbitraryType> values)
{
    // ...
}

The raw values in question are taken from the select menu options of the component, and can be any parseable data.

When using user, role, mentionable and channel select menus, no values are returned and the selected values are instead sent as resolved objects on the interaction response. Remora will handle this all for you automatically, with the expectation that you name your command parameters users, roles and channels respectively.

Please note that resolved data will contain partial objects for channels, although resolved users and roles are concrete. Hence, by using a non-partial interface on a channel command parameter (e.g. IReadOnlyList<IChannel> rather than IReadOnlyList<IPartialChannel>), you may incur additional network calls if the concrete channel objects are not already cached.

[SelectMenu("my-channel-select-menu")]
public Task<Result> OnMenuSelectionAsync(IReadOnlyList<IChannel> channels)
{
    // ...
}
private readonly InteractionContext _context; // Injected

[SelectMenu("my-mentionable-menu")]
public Task<Result> OnMenuSelectionAsync(IReadOnlyList<IChannel> channels)
{
    // `values` will contain the IDs of the selected users/roles

    if (!_context.Data.TryPickT1(out var components, out _))
        // error, expected message component data to be present

    if (!components.Resolved.IsDefined(out var resolvedData))
        // error, expected resolved data to be present on non-string select menus

    resolvedData.Users.IsDefined(...);
    resolvedData.Members.IsDefined(...);
    resolvedData.Roles.IsDefined(...);
}

Modals

Modals are functions with zero or more parameters matching the value types of the modal's contained components. This might sound a little complex, but is in practice quite easy to use.

Modal interactions fire when a user submits an opened modal containing some values.

For example, if your modal contains a TextInput component with a custom ID of my-text-input, you could then declare your function with a single parameter that matches that custom ID. This parameter would then be passed the value of that TextInput component.

[Modal("my-modal")]
public Task<Result> OnModalSubmittedAsync(string myTextInput)
{
    // ...
}

Multiple TextInput components map the same way, and can - same as with SelectMenu components - be declared as any parseable type. Parameters can even be provided with default values, which will be used if the modal does not contain a matching component.

[Modal("my-modal")]
public Task<Result> OnModalSubmittedAsync
(
    Snowflake mySnowflakeInput, 
    string myTextInput, 
    int myNumberInput = 0
)
{
    // ...
}

If you do not require any data from the modal, you can declare the function without any parameters. This is also useful when your parsing requirements are more complex than the default system supports, in which case the raw modal payload is available on the InteractionContext type. An instance of this type can be injected into your interaction group just like any other dependency or service.

[Modal("my-modal")]
public Task<Result> OnModalSubmittedAsync()
{
    // ...
}

Registering Your Interaction Group

Once you're ready to start using your interactions, it's as simple as registering it with your service collection and sending a component with an appropriately formatted custom ID.

services.AddInteractionGroup<MyInteractions>

Sending a Compatible Component

To avoid conflicts with components outside of Remora's interactivity system, you are required to use specially prefixed IDs when creating components for use with the system.

Custom IDs

These IDs can be created through the CustomIDHelpers class. For example, to create a compatible button, you would do something like the following.

new ButtonComponent
(
    ButtonComponentStyle.Primary,
    Label: "Click me!",
    CustomID: CustomIDHelpers.CreateButtonID("my-button")
)

Similar methods exist for the other component types, including modals.

There is one important exception to this - components inside a modal must not use any of the helper methods, and may only be comprised of a string convertible to a valid C# identifier. In practice, this means an ASCII string; dashes and underscores, however, may be used to delimit words, in which case they will be used for word boundaries when mapping to a camelCase name.

Some examples:

Custom ID Mapped C# Identifier
"text" text
"my-text" myText
"my_text" myText
Named Groups

If you wish to utilize named groups (perhaps for the sake of organization, or for distinguishing multiple interactions with the same desired ID), you can do so by specifying the group name or names before the custom ID, delimited by a space.

That is, a class like the one declared below:

[Group("separate")]
public class MyInteractions : InteractionGroup
{
    [Button("my-button")]
    public Task<Result> OnButtonPressedAsync()
    {
        // ...
    }
}

would respond to a button with a custom ID created like this:

CustomIDHelpers.CreateButtonID("separate my-button")

In-Memory Data

Many interactions will want to share some type of persistent state, but not all bots have the luxury or need of a full-fledged database behind them. Remora exposes a small type to aid with pure in-memory persistence that doesn't survive a restart, but can serve simpler purposes such as pagination.

In essence, the type is a wrapped ConcurrentDictionary with stronger guarantees related to data access while you have access to a contained value.

To use it, register a singleton instance of whichever types you want to store in your service provider.

services.AddSingleton(InMemoryDataService<MyKey, MyData>.Instance)

You can then inject this instance into your other services and groups in order to manipulate the data contained within. You can also access the singleton instance directly, though that should be avoided if at all possible.

The service has three CRUD-like methods for data manipulation:

bool TryAddData(TKey key, TData data);
Task<Result<DataLease<TKey, TData>>> LeaseDataAsync(TKey key, CancellationToken ct = default)
bool TryRemoveData(TKey key);
ValueTask<bool> TryRemoveDataAsync(TKey key);

Most signatures should be fairly self-explanatory, but there are some things to be mindful of. First and foremost, data is accessed by obtaining an exclusive lease from the container via LeaseDataAsync. This method produces a DataLease, which is a disposable wrapper type around the actual data. You may use the data for as long as you hold the lease (that is, it is not disposed), and provided everyone sticks to the same set of rules, you can be assured that you have exclusive access.

You can modify the value either by directly mutating it or by assigning a new value to the Data property on the lease. If you no longer require the data, you can also delete it by calling Delete on the lease.

Once you're done with the data, simply DisposeAsync (or await using) the lease. The lease will then either update the associated data in the container or delete it, depending on what you requested.

Any data passed into the service or a lease should be considered moved, and any further access after the lease expires is invalid on penalty of concurrency bugs and sad kittens. If you want to access the data again, you must also lease it once more.

Deleted data is, if required, disposed when removed from the container. The asynchronous disposal method takes precedence over the synchronous variant, but both are supported. If you data only supports asynchronous disposal, however, you cannot use TryRemoveData - it will throw an exception if it is unable to fully dispose of removed data. Prefer using the asynchronous alternative whenever possible.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on Remora.Discord.Interactivity:

Package Downloads
Remora.Discord

Metapackage for Remora.Discord's various components

Remora.Discord.Extensions

Utilities and components which extend upon Remora.Discord's base resources

Remora.Discord.Pagination

Button-based pagination of messages for Remora.Discord

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
5.0.0 16,466 9/21/2024
4.5.4 22,575 5/28/2024
4.5.3 25,354 2/5/2024
4.5.2 14,247 11/14/2023
4.5.1 29,200 7/24/2023
4.5.0 18,526 5/11/2023
4.4.6 1,823 3/20/2023
4.4.5 2,241 1/19/2023
4.4.4 1,617 1/10/2023
4.4.3 1,932 12/28/2022
4.4.2 1,321 12/13/2022
4.4.1 1,010 12/13/2022
4.4.0 1,026 12/10/2022
4.3.0 1,565 10/30/2022
4.2.6 16,819 9/2/2022
4.2.5 1,440 8/19/2022
4.2.4 1,596 7/28/2022
4.2.3 1,462 7/26/2022
4.2.2 1,326 7/26/2022
4.2.1 1,866 6/30/2022
4.2.0 1,364 6/30/2022
4.1.0 1,295 6/29/2022
4.0.0 1,358 6/27/2022
3.0.26 1,442 6/23/2022
3.0.25 1,926 6/20/2022
3.0.24 1,382 6/19/2022
3.0.23 1,338 6/18/2022
3.0.22 1,474 6/14/2022
3.0.21 1,398 6/8/2022
3.0.20 1,395 6/3/2022
3.0.19 1,414 5/25/2022
3.0.18 1,432 5/23/2022
3.0.17 1,525 5/15/2022
3.0.16 1,427 5/10/2022
3.0.15 1,386 5/9/2022
3.0.14 1,378 5/8/2022
3.0.13 1,461 5/3/2022
3.0.12 1,735 4/28/2022
3.0.11 1,368 4/28/2022
3.0.10 1,382 4/25/2022
3.0.9 1,822 4/18/2022
3.0.8 1,393 4/18/2022
3.0.7 1,556 4/2/2022
3.0.6 1,405 4/2/2022
3.0.5 1,452 3/21/2022
3.0.4 1,435 3/17/2022
3.0.3 1,349 3/15/2022
3.0.2 1,824 2/20/2022
3.0.1 1,339 2/20/2022
3.0.0 1,418 2/19/2022
3.0.0-rc1 671 2/14/2022
2.0.0 1,421 2/14/2022
1.0.4 1,653 1/27/2022
1.0.3 2,530 1/11/2022
1.0.2 34,471 1/2/2022
1.0.1 909 1/1/2022
1.0.0 1,018 12/29/2021

BREAKING: Rework deletion logic for data leases to prevent deadlocks.