Fixes for slash commands; Implemented Commander like interface for interactions
This commit is contained in:
parent
975d7b9473
commit
bcd12ee369
|
@ -1,7 +1,12 @@
|
|||
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 = [];
|
||||
|
@ -13,6 +18,8 @@ class Interactions {
|
|||
/// 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;
|
||||
|
@ -20,19 +27,43 @@ class Interactions {
|
|||
_client.options.dispatchRawShardEvent = true;
|
||||
_logger.info("Interactions ready");
|
||||
|
||||
client.onReady.listen((event) {
|
||||
client.onReady.listen((event) async {
|
||||
client.shardManager.rawEvent.listen((event) {
|
||||
if (event.rawData["op"] as int == 0) {
|
||||
if (event.rawData["t"] as String == "INTERACTION_CREATE") {
|
||||
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.
|
||||
|
@ -65,12 +96,15 @@ class Interactions {
|
|||
var failed = 0;
|
||||
for (final command in _commands) {
|
||||
if (!command.isRegistered) {
|
||||
await command._register().then((value) {
|
||||
this._events.onSlashCommandCreated.add(command);
|
||||
try {
|
||||
final registeredCommand = await command._register();
|
||||
this._events.onSlashCommandCreated.add(registeredCommand);
|
||||
|
||||
success++;
|
||||
}).catchError(() {
|
||||
} on HttpResponseError catch (e) {
|
||||
this._logger.severe("Failed registering command: ${e.toString()}");
|
||||
failed++;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_logger.info(
|
||||
|
|
|
@ -10,9 +10,8 @@ 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;
|
||||
|
||||
InteractionEvent._new(Nyxx client, Map<String, dynamic> rawJson) {
|
||||
this._client = client;
|
||||
this.interaction = Interaction._new(client, rawJson);
|
||||
InteractionEvent._new(this._client, Map<String, dynamic> rawJson) {
|
||||
this.interaction = Interaction._new(this._client, rawJson);
|
||||
|
||||
if (this.interaction.type == 1) {
|
||||
this._pong();
|
||||
|
@ -73,7 +72,7 @@ class InteractionEvent {
|
|||
}
|
||||
|
||||
/// Used to acknowledge a Interaction and send a response. Once this is sent you can then only send ChannelMessages. You can also set showSource to also print out the command the user entered.
|
||||
Future<void> reply({ dynamic content, EmbedBuilder? embed, bool? tts, AllowedMentions? allowedMentions, bool showSource = false, }) async {
|
||||
Future<void> reply({ dynamic content, EmbedBuilder? embed, bool? tts, AllowedMentions? allowedMentions, bool showSource = false, bool hidden = false}) async {
|
||||
if (DateTime.now().isBefore(this.receivedAt.add(const Duration(minutes: 15)))) {
|
||||
String url;
|
||||
if (hasResponded) {
|
||||
|
@ -87,6 +86,7 @@ class InteractionEvent {
|
|||
body: {
|
||||
"type": showSource ? 4 : 3,
|
||||
"data": {
|
||||
if (hidden) "flags": 1 << 6,
|
||||
"content": content,
|
||||
"embeds": embed != null ? [BuilderUtility.buildRawEmbed(embed)] : null,
|
||||
"allowed_mentions":
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// The Interaction data. e.g channel, guild and member
|
||||
class Interaction extends SnowflakeEntity implements Disposable {
|
||||
class Interaction extends SnowflakeEntity {
|
||||
/// Reference to bot instance.
|
||||
final Nyxx _client;
|
||||
|
||||
|
@ -27,54 +27,45 @@ class Interaction extends SnowflakeEntity implements Disposable {
|
|||
late final String name;
|
||||
|
||||
/// Args of the interaction
|
||||
late final Map<String, InteractionOption> args;
|
||||
late final Iterable<InteractionOption> args;
|
||||
|
||||
Interaction._new(
|
||||
this._client,
|
||||
Map<String, dynamic> raw,
|
||||
) : super(Snowflake(raw["id"])) {
|
||||
/// Id of command
|
||||
late final Snowflake commandId;
|
||||
|
||||
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"],
|
||||
),
|
||||
Snowflake(raw["guild_id"],),
|
||||
);
|
||||
|
||||
this.channel = CacheUtility.createCacheableTextChannel(
|
||||
_client,
|
||||
Snowflake(
|
||||
raw["channel_id"],
|
||||
),
|
||||
Snowflake(raw["channel_id"]),
|
||||
);
|
||||
|
||||
this.author = EntityUtility.createGuildMember(
|
||||
_client,
|
||||
Snowflake(
|
||||
raw["guild_id"],
|
||||
),
|
||||
Snowflake(raw["guild_id"]),
|
||||
raw["member"] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
this.token = raw["token"] as String;
|
||||
this.version = raw["version"] as int;
|
||||
this.name = raw["data"]["name"] as String;
|
||||
this.args = _generateArgs(raw["data"] as Map<String, dynamic>);
|
||||
this.commandId = Snowflake(raw["data"]["id"]);
|
||||
}
|
||||
|
||||
Map<String, InteractionOption> _generateArgs(Map<String, dynamic> rawData) {
|
||||
final args = <String, InteractionOption>{};
|
||||
|
||||
if (rawData["options"] != null) {
|
||||
final options = rawData["options"] as List;
|
||||
for (final option in options) {
|
||||
args[option["name"] as String] = InteractionOption._new(
|
||||
option["value"] as dynamic,
|
||||
(option["options"] ?? List<dynamic>.empty()) as List<Map<String, dynamic>>,
|
||||
);
|
||||
}
|
||||
Iterable<InteractionOption> _generateArgs(Map<String, dynamic> rawData) sync* {
|
||||
if (rawData["options"] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return args;
|
||||
final options = rawData["options"] as List<dynamic>;
|
||||
for (final option in options) {
|
||||
yield InteractionOption._new(option as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() => Future.value(null);
|
||||
}
|
||||
|
|
|
@ -3,17 +3,30 @@ part of nyxx_interactions;
|
|||
/// The option given by the user when sending a command
|
||||
class InteractionOption {
|
||||
/// The value given by the user
|
||||
final dynamic? value;
|
||||
late final dynamic value;
|
||||
|
||||
/// Type of interaction
|
||||
late final InteractionOption type;
|
||||
|
||||
/// Name of option
|
||||
late final String name;
|
||||
|
||||
/// Any args under this as you can have sub commands
|
||||
final Map<String, InteractionOption> args = {};
|
||||
late final Iterable<InteractionOption> args;
|
||||
|
||||
InteractionOption._new(this.value, List<Map<String, dynamic>> rawOptions) {
|
||||
for (final option in rawOptions) {
|
||||
this.args[option["name"] as String] = InteractionOption._new(
|
||||
option["value"] as dynamic,
|
||||
(option["options"] ?? List<Map<String, dynamic>>.empty()) as List<Map<String, dynamic>>,
|
||||
);
|
||||
/// Option choices
|
||||
late final Iterable<ArgChoice> choices;
|
||||
|
||||
InteractionOption._new(Map<String, dynamic> raw) {
|
||||
this.value = raw["value"] as dynamic;
|
||||
this.name = raw["name"] as String;
|
||||
|
||||
if (raw["options"] != null) {
|
||||
this.args = (raw["options"] as List<Map<String, dynamic>>).map((e) => InteractionOption._new(e));
|
||||
}
|
||||
|
||||
if (raw["choices"] != null) {
|
||||
this.choices = (raw["options"] as List<Map<String, dynamic>>).map((e) => ArgChoice(e["name"] as String, e["value"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,30 +2,51 @@ part of nyxx_interactions;
|
|||
|
||||
/// A slash command, can only be instantiated through a method on [Interactions]
|
||||
class SlashCommand {
|
||||
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
|
||||
late final String name;
|
||||
|
||||
/// Command description shown to the user in the Slash Command UI
|
||||
late 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
|
||||
late final List<CommandArg> args;
|
||||
|
||||
/// If the command is a global on, false if restricted to a guild.
|
||||
late final bool isGlobal;
|
||||
bool get isGlobal => this.guild == null;
|
||||
|
||||
/// If the command has been registered with the discord api
|
||||
late bool isRegistered = false;
|
||||
|
||||
late final Nyxx _client;
|
||||
|
||||
SlashCommand._new(this._client, this.name, this.description, this.args, {this.guild}) {
|
||||
this.isGlobal = guild == null;
|
||||
}
|
||||
SlashCommand._new(this._client, this.name, this.description, this.args, {this.guild});
|
||||
|
||||
Future<SlashCommand> _register() async {
|
||||
final options = args.map((e) => e._build());
|
||||
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(
|
||||
"/applications/${this._client.app.id.toString()}/commands",
|
||||
path,
|
||||
"POST",
|
||||
body: {"name": this.name, "description": this.description, "options": options.isNotEmpty ? options : null},
|
||||
);
|
||||
|
@ -34,6 +55,7 @@ class SlashCommand {
|
|||
return Future.error(response);
|
||||
}
|
||||
|
||||
this._id = Snowflake((response as HttpResponseSuccess).jsonBody["id"]);
|
||||
this.isRegistered = true;
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// A specified choice for a slash command argument.
|
||||
class ArgChoice {
|
||||
class ArgChoice implements Builder {
|
||||
/// This options name.
|
||||
late final String name;
|
||||
final String name;
|
||||
|
||||
/// This is the options value, must be int or string
|
||||
late final dynamic value;
|
||||
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, dynamic value) {
|
||||
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");
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _build() => {"name": this.name, "value": this.value};
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
part of nyxx_interactions;
|
||||
|
||||
/// The type that a user should input for a [SlashArg]
|
||||
enum CommandArgType {
|
||||
/// The type that a user should input for a [CommandArg]
|
||||
class CommandArgType extends IEnum<int> {
|
||||
/// Specify an arg as a sub command
|
||||
subCommand,
|
||||
static const subCommand = const CommandArgType(1);
|
||||
/// Specify an arg as a sub command group
|
||||
subCommandGroup,
|
||||
static const subCommandGroup = const CommandArgType(2);
|
||||
/// Specify an arg as a string
|
||||
string,
|
||||
static const string = const CommandArgType(3);
|
||||
/// Specify an arg as an int
|
||||
integer,
|
||||
static const integer = const CommandArgType(4);
|
||||
/// Specify an arg as a bool
|
||||
boolean,
|
||||
static const boolean = const CommandArgType(5);
|
||||
/// Specify an arg as a user e.g @HarryET#2954
|
||||
user,
|
||||
static const user = const CommandArgType(6);
|
||||
/// Specify an arg as a channel e.g. #Help
|
||||
channel,
|
||||
static const channel = const CommandArgType(7);
|
||||
/// Specify an arg as a role e.g. @RoleName
|
||||
role,
|
||||
static const role = const CommandArgType(8);
|
||||
|
||||
/// Create new instance of CommandArgType
|
||||
const CommandArgType(int value) : super(value);
|
||||
}
|
||||
|
||||
/// An argument for a [SlashCommand].
|
||||
class CommandArg {
|
||||
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 |
|
||||
/// |-------------------|-------|
|
||||
|
@ -57,23 +60,13 @@ class CommandArg {
|
|||
CommandArg(this.type, this.name, this.description,
|
||||
{this.defaultArg = false, this.required = false, this.choices, this.options});
|
||||
|
||||
Map<String, dynamic> _build() {
|
||||
final subOptions = this.options != null
|
||||
? this.options!.map((e) => e._build())
|
||||
: null;
|
||||
|
||||
final rawChoices = this.choices != null
|
||||
? this.choices!.map((e) => e._build())
|
||||
: null;
|
||||
|
||||
return {
|
||||
"type": (this.type.index) + 1,
|
||||
Map<String, dynamic> _build() => {
|
||||
"type": this.type.value,
|
||||
"name": this.name,
|
||||
"description": this.description,
|
||||
"default": this.defaultArg,
|
||||
"required": this.required,
|
||||
"choices": rawChoices,
|
||||
"options": subOptions
|
||||
if (this.choices != null) "choices": this.choices!.map((e) => e._build()),
|
||||
if (this.options != null) "options": this.options!.map((e) => e._build())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,3 +12,7 @@ environment:
|
|||
dependencies:
|
||||
logging: "^1.0.0-nullsafety.0"
|
||||
nyxx: "^1.1.0-dev.2"
|
||||
|
||||
dependency_overrides:
|
||||
nyxx:
|
||||
path: "../nyxx"
|
|
@ -218,7 +218,7 @@ class Nyxx implements Disposable {
|
|||
|
||||
final errorsPort = ReceivePort();
|
||||
errorsPort.listen((err) {
|
||||
_logger.severe("ERROR: ${err[0]} \n ${err[1]}");
|
||||
_logger.severe("ERROR: ${err[0]}; ${err[1]}");
|
||||
});
|
||||
Isolate.current.addErrorListener(errorsPort.sendPort);
|
||||
}
|
||||
|
|
|
@ -1441,17 +1441,9 @@ class _HttpEndpoints implements IHttpEndpoints {
|
|||
|
||||
@override
|
||||
Future<_HttpResponse> sendRawRequest(String url, String method,
|
||||
{dynamic body, dynamic headers}) async {
|
||||
final response = await _httpClient
|
||||
{dynamic body, dynamic headers}) => _httpClient
|
||||
._execute(BasicRequest._new(url, method: method, body: body));
|
||||
|
||||
if (response is HttpResponseError) {
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
return Future.value(response);
|
||||
}
|
||||
|
||||
Future<_HttpResponse> _getGatewayBot() =>
|
||||
_client._http._execute(BasicRequest._new("/gateway/bot"));
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ class HttpResponseSuccess extends _HttpResponse {
|
|||
|
||||
/// Returned when client fails to execute http request.
|
||||
/// Will contain reason why request failed.
|
||||
class HttpResponseError extends _HttpResponse {
|
||||
class HttpResponseError extends _HttpResponse implements Error {
|
||||
/// Message why http request failed
|
||||
late String errorMessage;
|
||||
|
||||
|
@ -71,4 +71,7 @@ class HttpResponseError extends _HttpResponse {
|
|||
@override
|
||||
String toString() =>
|
||||
"[Code: $errorCode] [Message: $errorMessage]";
|
||||
|
||||
@override
|
||||
StackTrace? get stackTrace => null;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue