Skip to content

Commit

Permalink
Merge pull request #48 from nyxx-discord/dev
Browse files Browse the repository at this point in the history
Release 4.1.0
  • Loading branch information
abitofevrything authored Apr 10, 2022
2 parents c203484 + defaf78 commit ef3edf3
Show file tree
Hide file tree
Showing 15 changed files with 589 additions and 102 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## 4.1.0
__New features__:
- Support for autocompletion has been added. See `Converter.autocompleteCallback` and the `@Autocomplete(...)` annotation for more.
- Added the ability to allow only slash commands or disable them entirely. See `CommandType.def` and `CommandOptions.defaultCommandType` for more.
- Added `ChatCommand.argumentTypes`, which allows developers to access the argument types for a chat command callback.
- Added `Converter.processOptionCallback`, which allows developers to modify the builder generated for a command argument.
- Added `IntConverter`, `DoubleConverter` and `NumConverter` for converting numbers with custom bounds. These new classes allow you to specify a minimum and maximum value for an argument when used with `@UseConverter(...)`.
- Added `GUildChannelConverter` for converting more specific types of guild channels.

__Bug fixes__:
- Fixed an issue with `IContext.getButtonPress` not behaving correectly when `authorOnly` or `timeout` was specified.
- Fixed the default converters for guild channels accepting all channels in the Discord UI even if they were not the correct type.

__Miscellaneous__:
- Updated the command name validation regex.
- Bump `nyxx_interactions` to 4.1.0.

## 4.0.0
__Breaking changes__:
- `nyxx_interactions` has been upgraded to 4.0.0.
Expand Down
10 changes: 8 additions & 2 deletions lib/nyxx_commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,22 @@ export 'src/commands/interfaces.dart'
export 'src/commands/message_command.dart' show MessageCommand;
export 'src/commands/options.dart' show CommandOptions;
export 'src/commands/user_command.dart' show UserCommand;
export 'src/context/autocomplete_context.dart' show AutocompleteContext;
export 'src/context/chat_context.dart'
show IChatContext, InteractionChatContext, MessageChatContext;
export 'src/context/context.dart' show IContext;
export 'src/context/interaction_context.dart' show IInteractionContext;
export 'src/context/context.dart' show IContext, IContextBase;
export 'src/context/interaction_context.dart' show IInteractionContext, IInteractionContextBase;
export 'src/context/message_context.dart' show MessageContext;
export 'src/context/user_context.dart' show UserContext;
export 'src/converters/converter.dart'
show
CombineConverter,
Converter,
DoubleConverter,
FallbackConverter,
GuildChannelConverter,
IntConverter,
NumConverter,
attachmentConverter,
boolConverter,
categoryGuildChannelConverter,
Expand All @@ -55,6 +60,7 @@ export 'src/converters/converter.dart'
parse;
export 'src/errors.dart'
show
AutocompleteFailedException,
BadInputException,
CheckFailedException,
CommandInvocationException,
Expand Down
111 changes: 103 additions & 8 deletions lib/src/commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ import 'commands/interfaces.dart';
import 'commands/message_command.dart';
import 'commands/user_command.dart';
import 'context/chat_context.dart';
import 'context/autocomplete_context.dart';
import 'context/context.dart';
import 'context/message_context.dart';
import 'context/user_context.dart';
import 'converters/converter.dart';
import 'errors.dart';
import 'options.dart';
import 'util/util.dart';
import 'util/view.dart';

final Logger logger = Logger('Commands');
Expand Down Expand Up @@ -327,6 +329,28 @@ class CommandsPlugin extends BasePlugin implements ICommandGroup<IContext> {
}
}

Future<void> _processAutocompleteInteraction(
IAutocompleteInteractionEvent interactionEvent,
FutureOr<Iterable<ArgChoiceBuilder>?> Function(AutocompleteContext) callback,
ChatCommand command,
) async {
try {
AutocompleteContext context = await _autocompleteContext(interactionEvent, command);

try {
Iterable<ArgChoiceBuilder>? choices = await callback(context);

if (choices != null) {
interactionEvent.respond(choices.toList());
}
} on Exception catch (e) {
throw AutocompleteFailedException(e, context);
}
} on CommandsException catch (e) {
_onCommandErrorController.add(e);
}
}

Future<IChatContext> _messageChatContext(
IMessage message, StringView contentView, String prefix) async {
ChatCommand command = getCommand(contentView) ?? (throw CommandNotFoundException(contentView));
Expand Down Expand Up @@ -452,14 +476,43 @@ class CommandsPlugin extends BasePlugin implements ICommandGroup<IContext> {
);
}

Future<AutocompleteContext> _autocompleteContext(
IAutocompleteInteractionEvent interactionEvent,
ChatCommand command,
) async {
ISlashCommandInteraction interaction = interactionEvent.interaction;

IMember? member = interaction.memberAuthor;
IUser user;
if (member != null) {
user = await member.user.getOrDownload();
} else {
user = interaction.userAuthor!;
}

return AutocompleteContext(
commands: this,
guild: await interaction.guild?.getOrDownload(),
channel: await interaction.channel.getOrDownload(),
member: member,
user: user,
command: command,
client: client!,
interaction: interaction,
interactionEvent: interactionEvent,
option: interactionEvent.focusedOption,
currentValue: interactionEvent.focusedOption.value.toString(),
);
}

Future<Iterable<SlashCommandBuilder>> _getSlashBuilders() async {
List<SlashCommandBuilder> builders = [];

const Snowflake zeroSnowflake = Snowflake.zero();

for (final command in children) {
if (command is IChatCommandComponent) {
if (command is ChatCommand && command.type == CommandType.textOnly) {
if (command is ChatCommand && command.resolvedType == CommandType.textOnly) {
continue;
}

Expand Down Expand Up @@ -510,6 +563,8 @@ class CommandsPlugin extends BasePlugin implements ICommandGroup<IContext> {

if (command is ChatCommand) {
builder.registerHandler((interaction) => _processChatInteraction(interaction, command));

_processAutocompleteHandlerRegistration(builder.options, command);
}

builders.add(builder);
Expand Down Expand Up @@ -593,12 +648,12 @@ class CommandsPlugin extends BasePlugin implements ICommandGroup<IContext> {
) {
for (final builder in options) {
if (builder.type == CommandOptionType.subCommand) {
builder.registerHandler(
(interaction) => _processChatInteraction(
interaction,
current.children.where((child) => child.name == builder.name).first as ChatCommand,
),
);
ChatCommand command =
current.children.where((child) => child.name == builder.name).first as ChatCommand;

builder.registerHandler((interaction) => _processChatInteraction(interaction, command));

_processAutocompleteHandlerRegistration(builder.options!, command);
} else if (builder.type == CommandOptionType.subCommandGroup) {
_processHandlerRegistration(
builder.options!,
Expand All @@ -610,6 +665,46 @@ class CommandsPlugin extends BasePlugin implements ICommandGroup<IContext> {
return options;
}

void _processAutocompleteHandlerRegistration(
Iterable<CommandOptionBuilder> options,
ChatCommand command,
) {
Iterator<CommandOptionBuilder> builderIterator = options.iterator;
Iterator<Type> argumentTypeIterator = command.argumentTypes.iterator;

MethodMirror mirror = (reflect(command.execute) as ClosureMirror).function;

// Skip context argument
Iterable<Autocomplete?> autocompleters = mirror.parameters.skip(1).map((parameter) {
Iterable<Autocomplete> annotations = parameter.metadata
.where((metadataMirror) => metadataMirror.hasReflectee)
.map((metadataMirror) => metadataMirror.reflectee)
.whereType<Autocomplete>();

if (annotations.isNotEmpty) {
return annotations.first;
}

return null;
});

Iterator<Autocomplete?> autocompletersIterator = autocompleters.iterator;

while (builderIterator.moveNext() &&
argumentTypeIterator.moveNext() &&
autocompletersIterator.moveNext()) {
FutureOr<Iterable<ArgChoiceBuilder>?> Function(AutocompleteContext)? autocompleteCallback =
autocompletersIterator.current?.callback;

autocompleteCallback ??= getConverter(argumentTypeIterator.current)?.autocompleteCallback;

if (autocompleteCallback != null) {
builderIterator.current.registerAutocompleteHandler(
(event) => _processAutocompleteInteraction(event, autocompleteCallback!, command));
}
}
}

/// Adds a converter to this [CommandsPlugin].
///
/// Converters can be used to convert user input ([String]s) to the type required by the command's
Expand Down Expand Up @@ -741,7 +836,7 @@ class CommandsPlugin extends BasePlugin implements ICommandGroup<IContext> {
if (_chatCommands.containsKey(name)) {
IChatCommandComponent child = _chatCommands[name]!;

if (child is ChatCommand && child.type != CommandType.slashOnly) {
if (child is ChatCommand && child.resolvedType != CommandType.slashOnly) {
ChatCommand? found = child.getCommand(view);

if (found == null) {
Expand Down
58 changes: 48 additions & 10 deletions lib/src/commands/chat_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ enum CommandType {

/// Indicates that a [ChatCommand] can be executed by both Slash Commands and text messages.
all,

/// Indicates that a [ChatCommand] should use the default type provided by [IOptions.options].
///
/// If the default type provided by the options is itself [def], the behaviour is identical to
/// [all].
// TODO: Instead of having [def], make [ChatCommand.type] be a classical option
// ([ChatCommand.options.type]) and have it be inherited.
def,
}

mixin ChatGroupMixin implements IChatCommandComponent {
Expand Down Expand Up @@ -126,7 +134,7 @@ mixin ChatGroupMixin implements IChatCommandComponent {
if (_childrenMap.containsKey(name)) {
IChatCommandComponent child = _childrenMap[name]!;

if (child is ChatCommand && child.type != CommandType.slashOnly) {
if (child is ChatCommand && child.resolvedType != CommandType.slashOnly) {
ChatCommand? found = child.getCommand(view);

if (found == null) {
Expand All @@ -152,7 +160,8 @@ mixin ChatGroupMixin implements IChatCommandComponent {

@override
bool get hasSlashCommand => children.any((child) =>
(child is ChatCommand && child.type != CommandType.textOnly) || child.hasSlashCommand);
(child is ChatCommand && child.resolvedType != CommandType.textOnly) ||
child.hasSlashCommand);

@override
Iterable<CommandOptionBuilder> getOptions(CommandsPlugin commands) {
Expand All @@ -166,7 +175,7 @@ mixin ChatGroupMixin implements IChatCommandComponent {
child.description,
options: List.of(child.getOptions(commands)),
));
} else if (child is ChatCommand && child.type != CommandType.textOnly) {
} else if (child is ChatCommand && child.resolvedType != CommandType.textOnly) {
options.add(CommandOptionBuilder(
CommandOptionType.subCommand,
child.name,
Expand Down Expand Up @@ -278,10 +287,26 @@ class ChatCommand
/// commands executable only through Slash Commands, or only through text messages.
///
/// You might also be interested in:
/// - [resolvedType], for getting the resolved type of this command.
/// - [ChatCommand.slashOnly], for creating [ChatCommand]s with type [CommandType.slashOnly];
/// - [ChatCommand.textOnly], for creating [ChatCommand]s with type [CommandType.textOnly].
final CommandType type;

/// The resolved type of this [ChatCommand].
///
/// If [type] is [CommandType.def], this will query the parent of this command for the default
/// type. Otherwise, [type] is returned.
///
/// If [type] is [CommandType.def] and no parent provides a default type, [CommandType.def] is
/// returned.
CommandType get resolvedType {
if (type != CommandType.def) {
return type;
}

return resolvedOptions.defaultCommandType ?? CommandType.def;
}

/// The function called to execute this command.
///
/// The argument types for the function are dynamically loaded, so you should specify the types of
Expand Down Expand Up @@ -314,6 +339,9 @@ class ChatCommand
/// - [checks] and [check], the equivalent for inherited checks.
final List<AbstractCheck> singleChecks = [];

/// The types of the required and positional arguments of [execute], in the order they appear.
final List<Type> argumentTypes = [];

@override
final CommandOptions options;

Expand All @@ -337,7 +365,7 @@ class ChatCommand
String description,
Function execute, {
List<String> aliases = const [],
CommandType type = CommandType.all,
CommandType type = CommandType.def,
Iterable<IChatCommandComponent> children = const [],
Iterable<AbstractCheck> checks = const [],
Iterable<AbstractCheck> singleChecks = const [],
Expand Down Expand Up @@ -407,7 +435,7 @@ class ChatCommand
this.execute,
Type contextType, {
this.aliases = const [],
this.type = CommandType.all,
this.type = CommandType.def,
Iterable<IChatCommandComponent> children = const [],
Iterable<AbstractCheck> checks = const [],
Iterable<AbstractCheck> singleChecks = const [],
Expand Down Expand Up @@ -481,9 +509,15 @@ class ChatCommand
throw CommandRegistrationError(
'Command callback parameters must not have more than one UseConverter annotation');
}
if (parametrer.metadata.where((element) => element.reflectee is Autocomplete).length > 1) {
throw CommandRegistrationError(
'Command callback parameters must not have more than one UseConverter annotation');
}
}

for (final argument in _arguments) {
argumentTypes.add(argument.type.reflectedType);

Iterable<Name> names = argument.metadata
.where((element) => element.reflectee is Name)
.map((nameMirror) => nameMirror.reflectee)
Expand Down Expand Up @@ -632,7 +666,7 @@ class ChatCommand

@override
Iterable<CommandOptionBuilder> getOptions(CommandsPlugin commands) {
if (type != CommandType.textOnly) {
if (resolvedType != CommandType.textOnly) {
List<CommandOptionBuilder> options = [];

for (final mirror in _arguments) {
Expand All @@ -657,13 +691,17 @@ class ChatCommand

choices ??= argumentConverter?.choices;

options.add(CommandOptionBuilder(
CommandOptionBuilder builder = CommandOptionBuilder(
argumentConverter?.type ?? CommandOptionType.string,
name,
_mappedDescriptions[name]!.value,
required: !mirror.isOptional,
choices: choices?.toList(),
));
);

argumentConverter?.processOptionCallback?.call(builder);

options.add(builder);
}

return options;
Expand All @@ -680,9 +718,9 @@ class ChatCommand
'All child commands of chat groups or commands must implement IChatCommandComponent');
}

if (type != CommandType.textOnly) {
if (resolvedType != CommandType.textOnly) {
if (command.hasSlashCommand ||
(command is ChatCommand && command.type != CommandType.textOnly)) {
(command is ChatCommand && command.resolvedType != CommandType.textOnly)) {
throw CommandRegistrationError('Cannot nest Slash commands!');
}
}
Expand Down
Loading

0 comments on commit ef3edf3

Please sign in to comment.