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
<PackageReference Include="Remora.Discord.Interactivity" Version="5.0.0" />
paket add Remora.Discord.Interactivity --version 5.0.0
#r "nuget: Remora.Discord.Interactivity, 5.0.0"
// 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 | Versions 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. |
-
.NETStandard 2.1
- Remora.Discord.Commands (>= 28.1.0)
- Remora.Discord.Gateway (>= 12.0.2)
-
net6.0
- Remora.Discord.Commands (>= 28.1.0)
- Remora.Discord.Gateway (>= 12.0.2)
-
net7.0
- Remora.Discord.Commands (>= 28.1.0)
- Remora.Discord.Gateway (>= 12.0.2)
-
net8.0
- Remora.Discord.Commands (>= 28.1.0)
- Remora.Discord.Gateway (>= 12.0.2)
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 | 13,340 | 9/21/2024 |
4.5.4 | 22,560 | 5/28/2024 |
4.5.3 | 25,263 | 2/5/2024 |
4.5.2 | 14,246 | 11/14/2023 |
4.5.1 | 29,195 | 7/24/2023 |
4.5.0 | 18,524 | 5/11/2023 |
4.4.6 | 1,822 | 3/20/2023 |
4.4.5 | 2,240 | 1/19/2023 |
4.4.4 | 1,616 | 1/10/2023 |
4.4.3 | 1,931 | 12/28/2022 |
4.4.2 | 1,320 | 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,425 | 5/10/2022 |
3.0.15 | 1,384 | 5/9/2022 |
3.0.14 | 1,376 | 5/8/2022 |
3.0.13 | 1,459 | 5/3/2022 |
3.0.12 | 1,733 | 4/28/2022 |
3.0.11 | 1,366 | 4/28/2022 |
3.0.10 | 1,380 | 4/25/2022 |
3.0.9 | 1,820 | 4/18/2022 |
3.0.8 | 1,391 | 4/18/2022 |
3.0.7 | 1,554 | 4/2/2022 |
3.0.6 | 1,403 | 4/2/2022 |
3.0.5 | 1,450 | 3/21/2022 |
3.0.4 | 1,433 | 3/17/2022 |
3.0.3 | 1,347 | 3/15/2022 |
3.0.2 | 1,822 | 2/20/2022 |
3.0.1 | 1,337 | 2/20/2022 |
3.0.0 | 1,416 | 2/19/2022 |
3.0.0-rc1 | 671 | 2/14/2022 |
2.0.0 | 1,419 | 2/14/2022 |
1.0.4 | 1,651 | 1/27/2022 |
1.0.3 | 2,528 | 1/11/2022 |
1.0.2 | 34,469 | 1/2/2022 |
1.0.1 | 907 | 1/1/2022 |
1.0.0 | 1,016 | 12/29/2021 |
BREAKING: Rework deletion logic for data leases to prevent deadlocks.