New slash commands API. subpackages directories are now consistent with names
This commit is contained in:
parent
1c30e31e63
commit
02e71ab27a
|
@ -81,36 +81,36 @@ jobs:
|
|||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./nyxx.commander
|
||||
working-directory: ./nyxx_commander
|
||||
run: dart pub get
|
||||
|
||||
- name: Analyze project source
|
||||
working-directory: ./nyxx.commander
|
||||
working-directory: ./nyxx_commander
|
||||
run: dart analyze
|
||||
|
||||
- name: Compile tests
|
||||
working-directory: ./nyxx.commander/test
|
||||
working-directory: ./nyxx_commander/test
|
||||
run: dart2native commander-test.dart
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./nyxx.commander/test
|
||||
working-directory: ./nyxx_commander/test
|
||||
run: ./commander-test.exe
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Generate docs
|
||||
working-directory: ./nyxx.commander
|
||||
working-directory: ./nyxx_commander
|
||||
run: dartdoc
|
||||
|
||||
- name: Deploy nyxx.commander dev docs
|
||||
- name: Deploy nyxx_commander dev docs
|
||||
uses: easingthemes/ssh-deploy@v2.1.5
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
|
||||
ARGS: "-rltDzvO"
|
||||
SOURCE: "nyxx.commander/doc/api/"
|
||||
SOURCE: "nyxx_commander/doc/api/"
|
||||
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.REMOTE_USER }}
|
||||
TARGET: "${{ secrets.REMOTE_TARGET }}/nyxx.commander/"
|
||||
TARGET: "${{ secrets.REMOTE_TARGET }}/nyxx_commander/"
|
||||
|
||||
test-extensions:
|
||||
name: Tests extensions package
|
||||
|
@ -134,36 +134,36 @@ jobs:
|
|||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./nyxx.extensions
|
||||
working-directory: ./nyxx_extensions
|
||||
run: dart pub get
|
||||
|
||||
- name: Analyze project source
|
||||
working-directory: ./nyxx.extensions
|
||||
working-directory: ./nyxx_extensions
|
||||
run: dart analyze
|
||||
|
||||
- name: Compile tests
|
||||
working-directory: ./nyxx.extensions/test
|
||||
working-directory: ./nyxx_extensions/test
|
||||
run: dart2native extensions-tests.dart
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./nyxx.extensions/test
|
||||
working-directory: ./nyxx_extensions/test
|
||||
run: ./extensions-tests.exe
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Generate docs
|
||||
working-directory: ./nyxx.extensions
|
||||
working-directory: ./nyxx_extensions
|
||||
run: dartdoc
|
||||
|
||||
- name: Deploy nyxx.extensions dev docs
|
||||
- name: Deploy nyxx_extensions dev docs
|
||||
uses: easingthemes/ssh-deploy@v2.1.5
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
|
||||
ARGS: "-rltDzvO"
|
||||
SOURCE: "nyxx.extensions/doc/api/"
|
||||
SOURCE: "nyxx_extensions/doc/api/"
|
||||
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.REMOTE_USER }}
|
||||
TARGET: "${{ secrets.REMOTE_TARGET }}/nyxx.extensions/"
|
||||
TARGET: "${{ secrets.REMOTE_TARGET }}/nyxx_extensions/"
|
||||
|
||||
test-interactions:
|
||||
name: Tests interactions package
|
||||
|
@ -187,23 +187,23 @@ jobs:
|
|||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./nyxx.interactions
|
||||
working-directory: ./nyxx_interactions
|
||||
run: dart pub get
|
||||
|
||||
- name: Analyze project source
|
||||
working-directory: ./nyxx.interactions
|
||||
working-directory: ./nyxx_interactions
|
||||
run: dart analyze
|
||||
|
||||
- name: Generate docs
|
||||
working-directory: ./nyxx.interactions
|
||||
working-directory: ./nyxx_interactions
|
||||
run: dartdoc
|
||||
|
||||
- name: Deploy nyxx.interactions dev docs
|
||||
- name: Deploy nyxx_interactions dev docs
|
||||
uses: easingthemes/ssh-deploy@v2.1.5
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
|
||||
ARGS: "-rltDzvO"
|
||||
SOURCE: "nyxx.interactions/doc/api/"
|
||||
SOURCE: "nyxx_interactions/doc/api/"
|
||||
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.REMOTE_USER }}
|
||||
TARGET: "${{ secrets.REMOTE_TARGET }}/nyxx.interactions/"
|
||||
TARGET: "${{ secrets.REMOTE_TARGET }}/nyxx_interactions/"
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import "package:nyxx/nyxx.dart";
|
||||
import "package:nyxx_interactions/interactions.dart";
|
||||
|
||||
void main() {
|
||||
final bot = Nyxx("<%TOKEN%>", GatewayIntents.all);
|
||||
|
||||
final interactions = Interactions(bot);
|
||||
|
||||
interactions.registerCommand(interactions.createCommand(
|
||||
"echo", // The command name
|
||||
"echo a message", // The commands description
|
||||
[CommandArg(CommandArgType.string, "message", "the message to be echoed.")], // The commands arguments
|
||||
guild: Snowflake(""), // Replace with your guilds ID
|
||||
));
|
||||
|
||||
bot.onReady.listen((event) {
|
||||
interactions.sync(); // Sync commands with API
|
||||
// Listen to slash commands being triggered
|
||||
interactions.onSlashCommand.listen((event) async {
|
||||
// Check if the name of the command is echo
|
||||
if (event.interaction.name == "echo") {
|
||||
// Reply with the message the user sent, showSource makes discord show the command the user sent in the channel.
|
||||
await event.respond(content: event.interaction.getArg("message"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import "package:nyxx/nyxx.dart";
|
||||
import "package:nyxx_interactions/interactions.dart";
|
||||
|
||||
void main() {
|
||||
final bot = Nyxx("<%TOKEN%>", GatewayIntents.all);
|
||||
final interactions = Interactions(bot);
|
||||
|
||||
interactions
|
||||
..registerHandler("test", "This is test comamnd", [], handler: (event) async {
|
||||
// Acknowledge about event so you can send reply later.
|
||||
// You have 3 second to either ack command or send response
|
||||
await event.acknowledge();
|
||||
|
||||
// After long running task, send response
|
||||
await event.respond(content: "This is example message result");
|
||||
});
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
typedef SlashCommandHandlder = FutureOr<void> Function(InteractionEvent);
|
||||
|
||||
/// Interaction extension for Nyxx. Allows use of: Slash Commands.
|
||||
class Interactions {
|
||||
static const _interactionCreateCommand = "INTERACTION_CREATE";
|
||||
static const _op0 = 0;
|
||||
|
||||
late final Nyxx _client;
|
||||
final Logger _logger = Logger("Interactions");
|
||||
final List<SlashCommand> _commands = [];
|
||||
late final _EventController _events;
|
||||
|
||||
/// Emitted when a slash command is sent.
|
||||
late final Stream<InteractionEvent> onSlashCommand;
|
||||
|
||||
/// Emitted when a slash command is created by the user.
|
||||
late final Stream<SlashCommand> onSlashCommandCreated;
|
||||
|
||||
final _commandHandlers = <String, SlashCommandHandlder>{};
|
||||
|
||||
/// Create new instance of the interactions class.
|
||||
Interactions(Nyxx client) {
|
||||
this._client = client;
|
||||
_events = _EventController(this);
|
||||
_client.options.dispatchRawShardEvent = true;
|
||||
_logger.info("Interactions ready");
|
||||
|
||||
client.onReady.listen((event) async {
|
||||
client.shardManager.rawEvent.listen((event) {
|
||||
if (event.rawData["op"] as int == _op0) {
|
||||
if (event.rawData["t"] as String == _interactionCreateCommand) {
|
||||
_events.onSlashCommand.add(
|
||||
InteractionEvent._new(client, event.rawData["d"] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this._commandHandlers.isNotEmpty) {
|
||||
await this.sync();
|
||||
this.onSlashCommand.listen((event) async {
|
||||
try {
|
||||
final handler = _commandHandlers[event.interaction.name];
|
||||
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await handler(event);
|
||||
} on Error catch (e) {
|
||||
this._logger.severe("Failed to execute command (${event.interaction.name})", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Registers command and handler for that command.
|
||||
void registerHandler(String name, String description, List<CommandArg> args, {required SlashCommandHandlder handler, Snowflake? guild}) {
|
||||
final command = this.createCommand(name, description, args, guild: guild);
|
||||
this.registerCommand(command);
|
||||
_commandHandlers[name] = handler;
|
||||
}
|
||||
|
||||
/// Creates a command that can be registered using .registerCommand or .registerCommands
|
||||
///
|
||||
/// The [name] is the name that the user can see when typing /, the [description] can also be seen in this same place. [args] are any arguments you want the user to type, you can put an empty list here is you require no arguments. If you want this to be specific to a guild you can set the [guild] param with the ID of a guild, when testing its recommended to use this as it propagates immediately while global commands can take some time.
|
||||
SlashCommand createCommand(String name, String description, List<CommandArg> args, {Snowflake? guild}) =>
|
||||
SlashCommand._new(_client, name, description, args,
|
||||
guild: guild != null ? CacheUtility.createCacheableGuild(_client, guild) : null);
|
||||
|
||||
/// Registers a single command.
|
||||
///
|
||||
/// The command you want to register is the [command] you create a command by using [createCommand]
|
||||
void registerCommand(SlashCommand command) => _commands.add(command);
|
||||
|
||||
/// Registers multiple commands at one.
|
||||
///
|
||||
/// The commands you want to register is the [commands] you create a command by using [createCommand], this just runs [registerCommand] for each command.
|
||||
void registerCommands(List<SlashCommand> commands) => commands.forEach(this.registerCommand);
|
||||
|
||||
/// Gets all the commands that are currently registered.
|
||||
List<SlashCommand> getCommands({bool registeredOnly = false}) {
|
||||
if (!registeredOnly) {
|
||||
return _commands;
|
||||
}
|
||||
|
||||
return _commands.where((command) => command.isRegistered).toList();
|
||||
}
|
||||
|
||||
/// Syncs the local commands with the discord API
|
||||
Future<void> sync() async {
|
||||
var success = 0;
|
||||
var failed = 0;
|
||||
for (final command in _commands) {
|
||||
if (!command.isRegistered) {
|
||||
try {
|
||||
final registeredCommand = await command._register();
|
||||
this._events.onSlashCommandCreated.add(registeredCommand);
|
||||
|
||||
success++;
|
||||
} on HttpResponseError catch (e) {
|
||||
this._logger.severe("Failed registering command: ${e.toString()}");
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
_logger.info(
|
||||
"Successfully registered $success ${success > 1 ? "commands" : "command"}. Failed registering $failed ${failed > 1 ? "commands" : "command"}.");
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// A slash command, can only be instantiated through a method on [Interactions]
|
||||
class SlashCommand {
|
||||
final Nyxx _client;
|
||||
|
||||
Snowflake? _id;
|
||||
|
||||
Snowflake get id {
|
||||
if (!this.isRegistered || _id == null) {
|
||||
throw new StateError("There is no id if command is not registered");
|
||||
}
|
||||
|
||||
return _id!;
|
||||
}
|
||||
|
||||
/// Command name to be shown to the user in the Slash Command UI
|
||||
final String name;
|
||||
|
||||
/// Command description shown to the user in the Slash Command UI
|
||||
final String description;
|
||||
|
||||
/// The guild that the slash Command is registered in. This can be null if its a global command.
|
||||
late final Cacheable<Snowflake, Guild>? guild;
|
||||
|
||||
/// The arguments that the command takes
|
||||
final List<CommandArg> args;
|
||||
|
||||
/// If the command is a global on, false if restricted to a guild.
|
||||
bool get isGlobal => this.guild == null;
|
||||
|
||||
/// If the command has been registered with the discord api
|
||||
late bool isRegistered = false;
|
||||
|
||||
SlashCommand._new(this._client, this.name, this.description, this.args, {this.guild});
|
||||
|
||||
Future<SlashCommand> _register() async {
|
||||
final options = args.map((e) => e._build()).toList();
|
||||
|
||||
var path = "/applications/${this._client.app.id.toString()}";
|
||||
|
||||
if (this.guild != null) {
|
||||
path += "/guilds/${this.guild!.id}";
|
||||
}
|
||||
|
||||
path += "/commands";
|
||||
|
||||
final response = await this._client.httpEndpoints.sendRawRequest(
|
||||
path,
|
||||
"POST",
|
||||
body: {"name": this.name, "description": this.description, "options": options.isNotEmpty ? options : null},
|
||||
);
|
||||
|
||||
if (response is HttpResponseError) {
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
this._id = Snowflake((response as HttpResponseSuccess).jsonBody["id"]);
|
||||
this.isRegistered = true;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// The type that a user should input for a [CommandArg]
|
||||
class CommandArgType extends IEnum<int> {
|
||||
/// Specify an arg as a sub command
|
||||
static const subCommand = const CommandArgType(1);
|
||||
/// Specify an arg as a sub command group
|
||||
static const subCommandGroup = const CommandArgType(2);
|
||||
/// Specify an arg as a string
|
||||
static const string = const CommandArgType(3);
|
||||
/// Specify an arg as an int
|
||||
static const integer = const CommandArgType(4);
|
||||
/// Specify an arg as a bool
|
||||
static const boolean = const CommandArgType(5);
|
||||
/// Specify an arg as a user e.g @HarryET#2954
|
||||
static const user = const CommandArgType(6);
|
||||
/// Specify an arg as a channel e.g. #Help
|
||||
static const channel = const CommandArgType(7);
|
||||
/// Specify an arg as a role e.g. @RoleName
|
||||
static const role = const CommandArgType(8);
|
||||
|
||||
/// Create new instance of CommandArgType
|
||||
const CommandArgType(int value) : super(value);
|
||||
}
|
||||
|
||||
/// An argument for a [SlashCommand].
|
||||
class CommandArg implements Builder {
|
||||
/// The type of arg that will be later changed to an INT value, their values can be seen in the table below:
|
||||
/// | Name | Value |
|
||||
/// |-------------------|-------|
|
||||
/// | SUB_COMMAND | 1 |
|
||||
/// | SUB_COMMAND_GROUP | 2 |
|
||||
/// | STRING | 3 |
|
||||
/// | INTEGER | 4 |
|
||||
/// | BOOLEAN | 5 |
|
||||
/// | USER | 6 |
|
||||
/// | CHANNEL | 7 |
|
||||
/// | ROLE | 8 |
|
||||
late final CommandArgType type;
|
||||
|
||||
/// The name of your argument / sub-group.
|
||||
late final String name;
|
||||
|
||||
/// The description of your argument / sub-group.
|
||||
late final String description;
|
||||
|
||||
/// If this should be the fist required option the user picks
|
||||
late final bool defaultArg;
|
||||
|
||||
/// If this argument is required
|
||||
late final bool required;
|
||||
|
||||
/// Choices for [CommandArgType.string] and [CommandArgType.string] types for the user to pick from
|
||||
late final List<ArgChoice>? choices;
|
||||
|
||||
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters
|
||||
late final List<CommandArg>? options;
|
||||
|
||||
/// Used to create an argument for a [SlashCommand]. Tease are used in [Interactions.registerCommand]
|
||||
CommandArg(this.type, this.name, this.description,
|
||||
{this.defaultArg = false, this.required = false, this.choices, this.options});
|
||||
|
||||
Map<String, dynamic> _build() => {
|
||||
"type": this.type.value,
|
||||
"name": this.name,
|
||||
"description": this.description,
|
||||
"default": this.defaultArg,
|
||||
"required": this.required,
|
||||
if (this.choices != null) "choices": this.choices!.map((e) => e._build()).toList(),
|
||||
if (this.options != null) "options": this.options!.map((e) => e._build()).toList()
|
||||
};
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
# nyxx.commander
|
||||
# nyxx_commander
|
||||
|
||||
[![pub](https://img.shields.io/pub/v/nyxx.svg)](https://pub.dartlang.org/packages/nyxx)
|
||||
[![documentation](https://img.shields.io/badge/Documentation-nyxx-yellow.svg)](https://www.dartdocs.org/documentation/nyxx/latest/)
|
|
@ -1,4 +1,4 @@
|
|||
# nyxx.extensions
|
||||
# nyxx_extensions
|
||||
|
||||
[![pub](https://img.shields.io/pub/v/nyxx.svg)](https://pub.dartlang.org/packages/nyxx)
|
||||
[![documentation](https://img.shields.io/badge/Documentation-nyxx-yellow.svg)](https://www.dartdocs.org/documentation/nyxx/latest/)
|
|
@ -1,4 +1,4 @@
|
|||
# nyxx.interactions
|
||||
# nyxx_interactions
|
||||
|
||||
[![pub](https://img.shields.io/pub/v/nyxx.svg)](https://pub.dartlang.org/packages/nyxx)
|
||||
[![documentation](https://img.shields.io/badge/Documentation-nyxx-yellow.svg)](https://www.dartdocs.org/documentation/nyxx/latest/)
|
|
@ -0,0 +1,27 @@
|
|||
import "package:nyxx/nyxx.dart";
|
||||
import "package:nyxx_interactions/interactions.dart";
|
||||
|
||||
void main() {
|
||||
// final bot = Nyxx("<%TOKEN%>", GatewayIntents.all);
|
||||
//
|
||||
// final interactions = Interactions(bot);
|
||||
//
|
||||
// interactions.registerCommand(SlashCommandBuilder(
|
||||
// "echo", // The command name
|
||||
// "echo a message", // The commands description
|
||||
// [CommandOptionBuilder(CommandOptionType.string, "message", "the message to be echoed.")], // The commands arguments
|
||||
// guild: Snowflake(""), // Replace with your guilds ID
|
||||
// ));
|
||||
//
|
||||
// bot.onReady.listen((event) {
|
||||
// interactions.sync(); // Sync commands with API
|
||||
// // Listen to slash commands being triggered
|
||||
// interactions.onSlashCommand.listen((event) async {
|
||||
// // Check if the name of the command is echo
|
||||
// if (event.interaction.name == "echo") {
|
||||
// // Reply with the message the user sent, showSource makes discord show the command the user sent in the channel.
|
||||
// await event.respond(content: event.interaction.getArg("message"));
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
}
|
|
@ -12,13 +12,19 @@ part "src/Interactions.dart";
|
|||
part "src/models/SlashCommand.dart";
|
||||
part "src/models/Interaction.dart";
|
||||
part "src/models/InteractionOption.dart";
|
||||
part "src/models/ArgChoice.dart";
|
||||
|
||||
// Builders
|
||||
part "src/builders/ArgChoiceBuilder.dart";
|
||||
part "src/builders/CommandOptionBuilder.dart";
|
||||
part "src/builders/SlashCommandBuilder.dart";
|
||||
|
||||
// Command Args
|
||||
part "src/models/commandArgs/ArgChoice.dart";
|
||||
part "src/models/commandArgs/CommandArg.dart";
|
||||
part "src/models/CommandOption.dart";
|
||||
|
||||
// Internal
|
||||
part "src/internal/_EventController.dart";
|
||||
part "src/internal/utils.dart";
|
||||
|
||||
// Events
|
||||
part "src/events/InteractionEvent.dart";
|
|
@ -0,0 +1,152 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
typedef SlashCommandHandlder = FutureOr<void> Function(InteractionEvent);
|
||||
|
||||
/// Interaction extension for Nyxx. Allows use of: Slash Commands.
|
||||
class Interactions {
|
||||
static const _interactionCreateCommand = "INTERACTION_CREATE";
|
||||
static const _op0 = 0;
|
||||
|
||||
final Nyxx _client;
|
||||
late final _EventController _events;
|
||||
|
||||
final Logger _logger = Logger("Interactions");
|
||||
|
||||
final _commandBuilders = <SlashCommandBuilder>[];
|
||||
final _commands = <SlashCommand>[];
|
||||
final _commandHandlers = <String, SlashCommandHandlder>{};
|
||||
|
||||
/// Emitted when a slash command is sent.
|
||||
late final Stream<InteractionEvent> onSlashCommand;
|
||||
|
||||
/// Emitted when a slash command is created by the user.
|
||||
late final Stream<SlashCommand> onSlashCommandCreated;
|
||||
|
||||
/// Create new instance of the interactions class.
|
||||
Interactions(this._client) {
|
||||
_events = _EventController(this);
|
||||
_client.options.dispatchRawShardEvent = true;
|
||||
_logger.info("Interactions ready");
|
||||
|
||||
_client.onReady.listen((event) async {
|
||||
_client.shardManager.rawEvent.listen((event) {
|
||||
if (event.rawData["op"] == _op0
|
||||
&& event.rawData["t"] == _interactionCreateCommand
|
||||
) {
|
||||
_events.onSlashCommand.add(InteractionEvent._new(_client, event.rawData["d"] as Map<String, dynamic>));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void syncOnReady() {
|
||||
this._client.onReady.listen((_) async {
|
||||
await this.sync();
|
||||
});
|
||||
}
|
||||
|
||||
/// Syncs command builders with discord
|
||||
Future<void> sync() async {
|
||||
final commandPartition = _partition<SlashCommandBuilder>(this._commandBuilders, (element) => element.guild == null);
|
||||
final globalCommands = commandPartition.first;
|
||||
final groupedGuildCommands = _groupSlashCommandBuilders(commandPartition.last);
|
||||
|
||||
final globalCommandsResponse = await this._client.httpEndpoints.sendRawRequest(
|
||||
"/applications/${this._client.app.id}/commands",
|
||||
"PUT",
|
||||
body: [
|
||||
for(final builder in globalCommands)
|
||||
builder._build()
|
||||
]
|
||||
);
|
||||
|
||||
if (globalCommandsResponse is HttpResponseSuccess) {
|
||||
this._registerCommandHandlers(globalCommandsResponse, globalCommands);
|
||||
}
|
||||
|
||||
for(final entry in groupedGuildCommands.entries) {
|
||||
final response = await this._client.httpEndpoints.sendRawRequest(
|
||||
"/applications/${this._client.app.id}/guilds/${entry.key}/commands",
|
||||
"PUT",
|
||||
body: [
|
||||
for(final builder in entry.value)
|
||||
builder._build()
|
||||
]
|
||||
);
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
this._registerCommandHandlers(response, entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
this._commandBuilders.clear(); // Cleanup after registering command since we don't need this anymore
|
||||
this._logger.info("Finished bulk overriding slash commands");
|
||||
|
||||
if (this._commands.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSlashCommand.listen((event) async {
|
||||
final commandHash = _determineInteractionCommandHandler(event.interaction);
|
||||
|
||||
if (this._commandHandlers.containsKey(commandHash)) {
|
||||
await this._commandHandlers[commandHash]!(event);
|
||||
}
|
||||
});
|
||||
|
||||
this._logger.info("Finished registering ${this._commandHandlers.length} commands!");
|
||||
}
|
||||
|
||||
void registerSlashCommand(SlashCommandBuilder slashCommandBuilder) {
|
||||
this._commandBuilders.add(slashCommandBuilder);
|
||||
}
|
||||
|
||||
void _registerCommandHandlers(HttpResponseSuccess response, Iterable<SlashCommandBuilder> builders) {
|
||||
final registeredSlashCommands = (response.jsonBody as List<dynamic>).map((e) => SlashCommand._new(e as Map<String, dynamic>, this._client));
|
||||
|
||||
for(final registeredCommand in registeredSlashCommands) {
|
||||
final matchingBuilder = builders.firstWhere((element) => element.name == registeredCommand.name);
|
||||
this._assignCommandToHandler(matchingBuilder, registeredCommand);
|
||||
|
||||
this._commands.add(registeredCommand);
|
||||
}
|
||||
}
|
||||
|
||||
void _assignCommandToHandler(SlashCommandBuilder builder, SlashCommand command) {
|
||||
final commandHashPrefix = "${command.id}|${command.name}";
|
||||
|
||||
final subCommands = builder.options.where((element) => element.type == CommandOptionType.subCommand);
|
||||
if (subCommands.isNotEmpty) {
|
||||
for (final subCommand in subCommands) {
|
||||
if (subCommand._handler == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._commandHandlers["$commandHashPrefix${subCommand.name}"] = subCommand._handler!;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final subCommandGroups = builder.options.where((element) => element.type == CommandOptionType.subCommandGroup);
|
||||
if (subCommandGroups.isNotEmpty) {
|
||||
for (final subCommandGroup in subCommandGroups) {
|
||||
final subCommands = subCommandGroup.options?.where((element) => element.type == CommandOptionType.subCommand) ?? [];
|
||||
|
||||
for (final subCommand in subCommands) {
|
||||
if (subCommand._handler == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._commandHandlers["$commandHashPrefix${subCommandGroup.name}${subCommand.name}"] = subCommand._handler!;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (builder._handler != null) {
|
||||
this._commandHandlers[commandHashPrefix] = builder._handler!;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// A specified choice for a slash command argument.
|
||||
class ArgChoice implements Builder {
|
||||
/// This options name.
|
||||
final String name;
|
||||
|
||||
/// This is the options value, must be int or string
|
||||
final dynamic value;
|
||||
|
||||
/// A Choice for the user to input in int & string args. You can only have an int or string option.
|
||||
ArgChoice(this.name, this.value) {
|
||||
if (value is! int && value is! String) {
|
||||
throw ArgumentError("Please send a string if its a string arg or an int if its an int arg");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _build() => {"name": this.name, "value": this.value};
|
||||
}
|
||||
part of nyxx_interactions;
|
||||
|
||||
/// A specified choice for a slash command argument.
|
||||
class ArgChoiceBuilder implements Builder {
|
||||
/// This options name.
|
||||
String name;
|
||||
|
||||
/// This is the options value, must be int or string
|
||||
dynamic value;
|
||||
|
||||
/// A Choice for the user to input in int & string args.
|
||||
/// You can only have an int or string option.
|
||||
ArgChoiceBuilder(this.name, this.value) {
|
||||
if (value is! int && value is! String) {
|
||||
throw ArgumentError("Please send a string if its a string arg or an int if its an int arg");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _build() => { "name": this.name, "value": this.value };
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// An argument for a [SlashCommandBuilder].
|
||||
class CommandOptionBuilder implements Builder {
|
||||
/// The type of arg that will be later changed to an INT value, their values can be seen in the table below:
|
||||
/// | Name | Value |
|
||||
/// |-------------------|-------|
|
||||
/// | SUB_COMMAND | 1 |
|
||||
/// | SUB_COMMAND_GROUP | 2 |
|
||||
/// | STRING | 3 |
|
||||
/// | INTEGER | 4 |
|
||||
/// | BOOLEAN | 5 |
|
||||
/// | USER | 6 |
|
||||
/// | CHANNEL | 7 |
|
||||
/// | ROLE | 8 |
|
||||
final CommandOptionType type;
|
||||
|
||||
/// The name of your argument / sub-group.
|
||||
final String name;
|
||||
|
||||
/// The description of your argument / sub-group.
|
||||
final String description;
|
||||
|
||||
/// If this should be the fist required option the user picks
|
||||
bool defaultArg = false;
|
||||
|
||||
/// If this argument is required
|
||||
bool required = false;
|
||||
|
||||
/// Choices for [CommandOptionType.string] and [CommandOptionType.string] types for the user to pick from
|
||||
List<ArgChoiceBuilder>? choices;
|
||||
|
||||
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters
|
||||
List<CommandOptionBuilder>? options;
|
||||
|
||||
SlashCommandHandlder? _handler;
|
||||
|
||||
/// Used to create an argument for a [SlashCommandBuilder].
|
||||
CommandOptionBuilder(this.type, this.name, this.description,
|
||||
{this.defaultArg = false, this.required = false, this.choices, this.options});
|
||||
|
||||
Map<String, dynamic> _build() => {
|
||||
"type": this.type.value,
|
||||
"name": this.name,
|
||||
"description": this.description,
|
||||
"default": this.defaultArg,
|
||||
"required": this.required,
|
||||
if (this.choices != null) "choices": this.choices!.map((e) => e._build()).toList(),
|
||||
if (this.options != null) "options": this.options!.map((e) => e._build()).toList()
|
||||
};
|
||||
|
||||
/// Registers handler for subcommand
|
||||
void registerHandler(SlashCommandHandlder handler) {
|
||||
if (this.type != CommandOptionType.subCommand) {
|
||||
throw StateError("Cannot register handler for command option with type other that subcommand");
|
||||
}
|
||||
|
||||
this._handler = handler;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// A slash command, can only be instantiated through a method on [Interactions]
|
||||
class SlashCommandBuilder implements Builder {
|
||||
/// Command name to be shown to the user in the Slash Command UI
|
||||
final String name;
|
||||
|
||||
/// Command description shown to the user in the Slash Command UI
|
||||
final String description;
|
||||
|
||||
/// The guild that the slash Command is registered in. This can be null if its a global command.
|
||||
Snowflake? guild;
|
||||
|
||||
/// The arguments that the command takes
|
||||
List<CommandOptionBuilder> options;
|
||||
|
||||
SlashCommandHandlder? _handler;
|
||||
|
||||
/// A slash command, can only be instantiated through a method on [Interactions]
|
||||
SlashCommandBuilder(this.name, this.description, this.options, {this.guild});
|
||||
|
||||
Map<String, dynamic> _build() => {
|
||||
"name": this.name,
|
||||
"description": this.description,
|
||||
if (this.options.isNotEmpty) "options": this.options.map((e) => e._build()).toList()
|
||||
};
|
||||
|
||||
void registerHandler(SlashCommandHandlder handler) => this._handler = handler;
|
||||
}
|
|
@ -13,19 +13,6 @@ class InteractionEvent {
|
|||
/// If the Client has sent a response to the Discord API. Once the API was received a response you cannot send another.
|
||||
bool hasResponded = false;
|
||||
|
||||
/// Returns subcommand or null if not subcommand
|
||||
InteractionOption? get subCommand {
|
||||
if (this.interaction.args.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return this.interaction.args.firstWhere((element) => element.type == CommandArgType.subCommand);
|
||||
} on Error {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
InteractionEvent._new(this._client, Map<String, dynamic> rawJson) {
|
||||
this.interaction = Interaction._new(this._client, rawJson);
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
Iterable<Iterable<T>> _partition<T>(Iterable<T> list, bool Function(T) predicate) {
|
||||
final matches = <T>[];
|
||||
final nonMatches = <T>[];
|
||||
|
||||
for(final e in list) {
|
||||
if(predicate(e)) {
|
||||
matches.add(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
nonMatches.add(e);
|
||||
}
|
||||
|
||||
return [matches, nonMatches];
|
||||
}
|
||||
|
||||
String _determineInteractionCommandHandler(Interaction interaction) {
|
||||
final commandHash = "${interaction.commandId}|${interaction.name}";
|
||||
|
||||
try {
|
||||
final subCommandGroup = interaction.args.firstWhere((element) => element.type == CommandOptionType.subCommandGroup);
|
||||
final subCommand = interaction.args.firstWhere((element) => element.type == CommandOptionType.subCommand);
|
||||
|
||||
return "$commandHash${subCommandGroup.name}${subCommand.name}";
|
||||
// ignore: empty_catches
|
||||
} on Error { }
|
||||
|
||||
final subCommand = interaction.args.firstWhere((element) => element.type == CommandOptionType.subCommand);
|
||||
|
||||
return "$commandHash${subCommand.name}";
|
||||
}
|
||||
|
||||
Map<Snowflake, Iterable<SlashCommandBuilder>> _groupSlashCommandBuilders(Iterable<SlashCommandBuilder> commands) {
|
||||
final commandsMap = <Snowflake, List<SlashCommandBuilder>>{};
|
||||
|
||||
for(final slashCommand in commands) {
|
||||
final id = slashCommand.guild!;
|
||||
|
||||
if (commandsMap.containsKey(id)) {
|
||||
commandsMap[id]!.add(slashCommand);
|
||||
continue;
|
||||
}
|
||||
|
||||
commandsMap[id] = [slashCommand];
|
||||
}
|
||||
|
||||
return commandsMap;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
class ArgChoice {
|
||||
late final String name;
|
||||
|
||||
late final dynamic value;
|
||||
|
||||
ArgChoice._new(Map<String, dynamic> raw) {
|
||||
this.name = raw["name"] as String;
|
||||
this.value = raw["value"];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// The type that a user should input for a [CommandOptionBuilder]
|
||||
class CommandOptionType extends IEnum<int> {
|
||||
/// Specify an arg as a sub command
|
||||
static const subCommand = const CommandOptionType(1);
|
||||
/// Specify an arg as a sub command group
|
||||
static const subCommandGroup = const CommandOptionType(2);
|
||||
/// Specify an arg as a string
|
||||
static const string = const CommandOptionType(3);
|
||||
/// Specify an arg as an int
|
||||
static const integer = const CommandOptionType(4);
|
||||
/// Specify an arg as a bool
|
||||
static const boolean = const CommandOptionType(5);
|
||||
/// Specify an arg as a user e.g @HarryET#2954
|
||||
static const user = const CommandOptionType(6);
|
||||
/// Specify an arg as a channel e.g. #Help
|
||||
static const channel = const CommandOptionType(7);
|
||||
/// Specify an arg as a role e.g. @RoleName
|
||||
static const role = const CommandOptionType(8);
|
||||
|
||||
/// Create new instance of CommandArgType
|
||||
const CommandOptionType(int value) : super(value);
|
||||
}
|
||||
|
||||
class CommandOption {
|
||||
/// The type of arg that will be later changed to an INT value, their values can be seen in the table below:
|
||||
/// | Name | Value |
|
||||
/// |-------------------|-------|
|
||||
/// | SUB_COMMAND | 1 |
|
||||
/// | SUB_COMMAND_GROUP | 2 |
|
||||
/// | STRING | 3 |
|
||||
/// | INTEGER | 4 |
|
||||
/// | BOOLEAN | 5 |
|
||||
/// | USER | 6 |
|
||||
/// | CHANNEL | 7 |
|
||||
/// | ROLE | 8 |
|
||||
late final CommandOptionType type;
|
||||
|
||||
/// The name of your argument / sub-group.
|
||||
late final String name;
|
||||
|
||||
/// The description of your argument / sub-group.
|
||||
late final String description;
|
||||
|
||||
/// If this argument is required
|
||||
late final bool required;
|
||||
|
||||
/// Choices for [CommandOptionType.string] and [CommandOptionType.string] types for the user to pick from
|
||||
late final List<ArgChoice> choices;
|
||||
|
||||
/// If the option is a subcommand or subcommand group type, this nested options will be the parameters
|
||||
late final List<CommandOption> options;
|
||||
|
||||
CommandOption._new(Map<String, dynamic> raw) {
|
||||
this.type = CommandOptionType(raw["type"] as int);
|
||||
this.name = raw["name"] as String;
|
||||
this.description = raw["description"] as String;
|
||||
this.required = raw["required"] as bool? ?? false;
|
||||
|
||||
this.choices = [
|
||||
if (raw["choices"] != null)
|
||||
for(final choiceRaw in raw["choices"])
|
||||
ArgChoice._new(choiceRaw as Map<String, dynamic>)
|
||||
];
|
||||
|
||||
this.options = [
|
||||
if (raw["options"] != null)
|
||||
for(final optionRaw in raw["options"])
|
||||
CommandOption._new(optionRaw as Map<String, dynamic>)
|
||||
];
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ class Interaction extends SnowflakeEntity {
|
|||
late final int type;
|
||||
|
||||
/// The guild the command was sent in.
|
||||
late final Cacheable<Snowflake, Guild> guild;
|
||||
late final Cacheable<Snowflake, Guild>? guild;
|
||||
|
||||
/// The channel the command was sent in.
|
||||
late final Cacheable<Snowflake, TextChannel> channel;
|
||||
|
@ -35,10 +35,14 @@ class Interaction extends SnowflakeEntity {
|
|||
Interaction._new(this._client, Map<String, dynamic> raw) : super(Snowflake(raw["id"])) {
|
||||
this.type = raw["type"] as int;
|
||||
|
||||
this.guild = CacheUtility.createCacheableGuild(
|
||||
_client,
|
||||
Snowflake(raw["guild_id"],),
|
||||
);
|
||||
if (raw["guild_id"] != null) {
|
||||
this.guild = CacheUtility.createCacheableGuild(
|
||||
_client,
|
||||
Snowflake(raw["guild_id"]),
|
||||
);
|
||||
} else {
|
||||
this.guild = null;
|
||||
}
|
||||
|
||||
this.channel = CacheUtility.createCacheableTextChannel(
|
||||
_client,
|
|
@ -6,7 +6,7 @@ class InteractionOption {
|
|||
late final dynamic value;
|
||||
|
||||
/// Type of interaction
|
||||
late final CommandArgType type;
|
||||
late final CommandOptionType type;
|
||||
|
||||
/// Name of option
|
||||
late final String name;
|
||||
|
@ -15,19 +15,19 @@ class InteractionOption {
|
|||
late final Iterable<InteractionOption> args;
|
||||
|
||||
/// Option choices
|
||||
late final Iterable<ArgChoice> choices;
|
||||
late final Iterable<ArgChoiceBuilder> choices;
|
||||
|
||||
InteractionOption._new(Map<String, dynamic> raw) {
|
||||
this.value = raw["value"] as dynamic;
|
||||
this.name = raw["name"] as String;
|
||||
this.type = CommandArgType(raw["type"] as int);
|
||||
this.type = CommandOptionType(raw["type"] as int);
|
||||
|
||||
if (raw["options"] != null) {
|
||||
this.args = (raw["options"] as List<dynamic>).map((e) => InteractionOption._new(e as Map<String, dynamic>));
|
||||
}
|
||||
|
||||
if (raw["choices"] != null) {
|
||||
this.choices = (raw["options"] as List<Map<String, dynamic>>).map((e) => ArgChoice(e["name"] as String, e["value"]));
|
||||
this.choices = (raw["options"] as List<Map<String, dynamic>>).map((e) => ArgChoiceBuilder(e["name"] as String, e["value"]));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
class SlashCommand extends SnowflakeEntity {
|
||||
/// Unique id of the parent application
|
||||
late final Snowflake applicationId;
|
||||
|
||||
/// Command name to be shown to the user in the Slash Command UI
|
||||
late final String name;
|
||||
|
||||
/// Command description shown to the user in the Slash Command UI
|
||||
late final String description;
|
||||
|
||||
/// The arguments that the command takes
|
||||
late final List<CommandOption> options;
|
||||
|
||||
SlashCommand._new(Map<String, dynamic> raw, Nyxx client): super(Snowflake(raw["id"])) {
|
||||
this.applicationId = Snowflake(raw["application_id"]);
|
||||
this.name = raw["name"] as String;
|
||||
this.description = raw["description"] as String;
|
||||
|
||||
this.options = [
|
||||
if (raw["options"] != null)
|
||||
for(final optionRaw in raw["options"])
|
||||
CommandOption._new(optionRaw as Map<String, dynamic>)
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue