New slash commands API. subpackages directories are now consistent with names

This commit is contained in:
Szymon Uglis 2021-04-16 01:42:55 +02:00
parent 1c30e31e63
commit 02e71ab27a
No known key found for this signature in database
GPG Key ID: AA55C76D0E11A7E3
60 changed files with 496 additions and 359 deletions

View File

@ -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/"

View File

@ -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"));
}
});
});
}

View File

@ -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");
});
}

View File

@ -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"}.");
}
}

View File

@ -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;
}
}

View File

@ -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()
};
}

View File

@ -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/)

View File

@ -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/)

View File

@ -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/)

View File

@ -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"));
// }
// });
// });
}

View File

@ -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";

View File

@ -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!;
}
}
}

View File

@ -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 };
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

@ -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"];
}
}

View File

@ -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>)
];
}
}

View File

@ -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,

View File

@ -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"]));
}
}
}

View File

@ -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>)
];
}
}