diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 772e1c6b..fad5e8f3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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/" diff --git a/nyxx.interactions/example/interactions-basic.dart b/nyxx.interactions/example/interactions-basic.dart deleted file mode 100644 index fe4194cf..00000000 --- a/nyxx.interactions/example/interactions-basic.dart +++ /dev/null @@ -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")); - } - }); - }); -} diff --git a/nyxx.interactions/interactions.dart b/nyxx.interactions/interactions.dart deleted file mode 100644 index ebfac977..00000000 --- a/nyxx.interactions/interactions.dart +++ /dev/null @@ -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"); - }); -} diff --git a/nyxx.interactions/lib/src/Interactions.dart b/nyxx.interactions/lib/src/Interactions.dart deleted file mode 100644 index 314cf6b3..00000000 --- a/nyxx.interactions/lib/src/Interactions.dart +++ /dev/null @@ -1,113 +0,0 @@ -part of nyxx_interactions; - -typedef SlashCommandHandlder = FutureOr 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 _commands = []; - late final _EventController _events; - - /// Emitted when a slash command is sent. - late final Stream onSlashCommand; - - /// Emitted when a slash command is created by the user. - late final Stream onSlashCommandCreated; - - final _commandHandlers = {}; - - /// 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), - ); - } - } - }); - - 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 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 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 commands) => commands.forEach(this.registerCommand); - - /// Gets all the commands that are currently registered. - List getCommands({bool registeredOnly = false}) { - if (!registeredOnly) { - return _commands; - } - - return _commands.where((command) => command.isRegistered).toList(); - } - - /// Syncs the local commands with the discord API - Future 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"}."); - } -} diff --git a/nyxx.interactions/lib/src/models/SlashCommand.dart b/nyxx.interactions/lib/src/models/SlashCommand.dart deleted file mode 100644 index 1fd94e63..00000000 --- a/nyxx.interactions/lib/src/models/SlashCommand.dart +++ /dev/null @@ -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? guild; - - /// The arguments that the command takes - final List 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 _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; - } -} diff --git a/nyxx.interactions/lib/src/models/commandArgs/CommandArg.dart b/nyxx.interactions/lib/src/models/commandArgs/CommandArg.dart deleted file mode 100644 index c89015ee..00000000 --- a/nyxx.interactions/lib/src/models/commandArgs/CommandArg.dart +++ /dev/null @@ -1,72 +0,0 @@ -part of nyxx_interactions; - -/// The type that a user should input for a [CommandArg] -class CommandArgType extends IEnum { - /// 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? choices; - - /// If the option is a subcommand or subcommand group type, this nested options will be the parameters - late final List? 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 _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() - }; -} diff --git a/nyxx.commander/CHANGELOG.md b/nyxx_commander/CHANGELOG.md similarity index 100% rename from nyxx.commander/CHANGELOG.md rename to nyxx_commander/CHANGELOG.md diff --git a/nyxx.commander/LICENSE b/nyxx_commander/LICENSE similarity index 100% rename from nyxx.commander/LICENSE rename to nyxx_commander/LICENSE diff --git a/nyxx.commander/README.md b/nyxx_commander/README.md similarity index 99% rename from nyxx.commander/README.md rename to nyxx_commander/README.md index 0d8324fe..46965816 100644 --- a/nyxx.commander/README.md +++ b/nyxx_commander/README.md @@ -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/) diff --git a/nyxx.commander/analysis_options.yaml b/nyxx_commander/analysis_options.yaml similarity index 100% rename from nyxx.commander/analysis_options.yaml rename to nyxx_commander/analysis_options.yaml diff --git a/nyxx.commander/lib/commander.dart b/nyxx_commander/lib/commander.dart similarity index 100% rename from nyxx.commander/lib/commander.dart rename to nyxx_commander/lib/commander.dart diff --git a/nyxx.commander/lib/src/CommandContext.dart b/nyxx_commander/lib/src/CommandContext.dart similarity index 100% rename from nyxx.commander/lib/src/CommandContext.dart rename to nyxx_commander/lib/src/CommandContext.dart diff --git a/nyxx.commander/lib/src/CommandHandler.dart b/nyxx_commander/lib/src/CommandHandler.dart similarity index 100% rename from nyxx.commander/lib/src/CommandHandler.dart rename to nyxx_commander/lib/src/CommandHandler.dart diff --git a/nyxx.commander/lib/src/Commander.dart b/nyxx_commander/lib/src/Commander.dart similarity index 100% rename from nyxx.commander/lib/src/Commander.dart rename to nyxx_commander/lib/src/Commander.dart diff --git a/nyxx.commander/lib/src/utils.dart b/nyxx_commander/lib/src/utils.dart similarity index 100% rename from nyxx.commander/lib/src/utils.dart rename to nyxx_commander/lib/src/utils.dart diff --git a/nyxx.commander/pubspec.yaml b/nyxx_commander/pubspec.yaml similarity index 100% rename from nyxx.commander/pubspec.yaml rename to nyxx_commander/pubspec.yaml diff --git a/nyxx.commander/test/commander-test.dart b/nyxx_commander/test/commander-test.dart similarity index 100% rename from nyxx.commander/test/commander-test.dart rename to nyxx_commander/test/commander-test.dart diff --git a/nyxx.extensions/CHANGELOG.md b/nyxx_extensions/CHANGELOG.md similarity index 100% rename from nyxx.extensions/CHANGELOG.md rename to nyxx_extensions/CHANGELOG.md diff --git a/nyxx.extensions/LICENSE b/nyxx_extensions/LICENSE similarity index 100% rename from nyxx.extensions/LICENSE rename to nyxx_extensions/LICENSE diff --git a/nyxx.extensions/README.md b/nyxx_extensions/README.md similarity index 99% rename from nyxx.extensions/README.md rename to nyxx_extensions/README.md index 212c09f4..a3b0ef5f 100644 --- a/nyxx.extensions/README.md +++ b/nyxx_extensions/README.md @@ -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/) diff --git a/nyxx.extensions/analysis_options.yaml b/nyxx_extensions/analysis_options.yaml similarity index 100% rename from nyxx.extensions/analysis_options.yaml rename to nyxx_extensions/analysis_options.yaml diff --git a/nyxx.extensions/lib/attachment_extension.dart b/nyxx_extensions/lib/attachment_extension.dart similarity index 100% rename from nyxx.extensions/lib/attachment_extension.dart rename to nyxx_extensions/lib/attachment_extension.dart diff --git a/nyxx.extensions/lib/emoji.dart b/nyxx_extensions/lib/emoji.dart similarity index 100% rename from nyxx.extensions/lib/emoji.dart rename to nyxx_extensions/lib/emoji.dart diff --git a/nyxx.extensions/lib/member_extension.dart b/nyxx_extensions/lib/member_extension.dart similarity index 100% rename from nyxx.extensions/lib/member_extension.dart rename to nyxx_extensions/lib/member_extension.dart diff --git a/nyxx.extensions/lib/message_resolver.dart b/nyxx_extensions/lib/message_resolver.dart similarity index 100% rename from nyxx.extensions/lib/message_resolver.dart rename to nyxx_extensions/lib/message_resolver.dart diff --git a/nyxx.extensions/lib/pagination.dart b/nyxx_extensions/lib/pagination.dart similarity index 100% rename from nyxx.extensions/lib/pagination.dart rename to nyxx_extensions/lib/pagination.dart diff --git a/nyxx.extensions/lib/scheduled_event.dart b/nyxx_extensions/lib/scheduled_event.dart similarity index 100% rename from nyxx.extensions/lib/scheduled_event.dart rename to nyxx_extensions/lib/scheduled_event.dart diff --git a/nyxx.extensions/lib/src/attachment_extension.dart b/nyxx_extensions/lib/src/attachment_extension.dart similarity index 100% rename from nyxx.extensions/lib/src/attachment_extension.dart rename to nyxx_extensions/lib/src/attachment_extension.dart diff --git a/nyxx.extensions/lib/src/emoji/EmojiDefinition.dart b/nyxx_extensions/lib/src/emoji/EmojiDefinition.dart similarity index 100% rename from nyxx.extensions/lib/src/emoji/EmojiDefinition.dart rename to nyxx_extensions/lib/src/emoji/EmojiDefinition.dart diff --git a/nyxx.extensions/lib/src/emoji/EmojiUtils.dart b/nyxx_extensions/lib/src/emoji/EmojiUtils.dart similarity index 100% rename from nyxx.extensions/lib/src/emoji/EmojiUtils.dart rename to nyxx_extensions/lib/src/emoji/EmojiUtils.dart diff --git a/nyxx.extensions/lib/src/member_extension.dart b/nyxx_extensions/lib/src/member_extension.dart similarity index 100% rename from nyxx.extensions/lib/src/member_extension.dart rename to nyxx_extensions/lib/src/member_extension.dart diff --git a/nyxx.extensions/lib/src/message_resolver/Regexes.dart b/nyxx_extensions/lib/src/message_resolver/Regexes.dart similarity index 100% rename from nyxx.extensions/lib/src/message_resolver/Regexes.dart rename to nyxx_extensions/lib/src/message_resolver/Regexes.dart diff --git a/nyxx.extensions/lib/src/message_resolver/message_resolver.dart b/nyxx_extensions/lib/src/message_resolver/message_resolver.dart similarity index 100% rename from nyxx.extensions/lib/src/message_resolver/message_resolver.dart rename to nyxx_extensions/lib/src/message_resolver/message_resolver.dart diff --git a/nyxx.extensions/lib/src/pagination/pagination.dart b/nyxx_extensions/lib/src/pagination/pagination.dart similarity index 100% rename from nyxx.extensions/lib/src/pagination/pagination.dart rename to nyxx_extensions/lib/src/pagination/pagination.dart diff --git a/nyxx.extensions/lib/src/scheduler/scheduler.dart b/nyxx_extensions/lib/src/scheduler/scheduler.dart similarity index 100% rename from nyxx.extensions/lib/src/scheduler/scheduler.dart rename to nyxx_extensions/lib/src/scheduler/scheduler.dart diff --git a/nyxx.extensions/lib/src/utils.dart b/nyxx_extensions/lib/src/utils.dart similarity index 100% rename from nyxx.extensions/lib/src/utils.dart rename to nyxx_extensions/lib/src/utils.dart diff --git a/nyxx.extensions/lib/utils.dart b/nyxx_extensions/lib/utils.dart similarity index 100% rename from nyxx.extensions/lib/utils.dart rename to nyxx_extensions/lib/utils.dart diff --git a/nyxx.extensions/pubspec.yaml b/nyxx_extensions/pubspec.yaml similarity index 100% rename from nyxx.extensions/pubspec.yaml rename to nyxx_extensions/pubspec.yaml diff --git a/nyxx.extensions/test/extensions-tests.dart b/nyxx_extensions/test/extensions-tests.dart similarity index 100% rename from nyxx.extensions/test/extensions-tests.dart rename to nyxx_extensions/test/extensions-tests.dart diff --git a/nyxx.interactions/CHANGELOG.md b/nyxx_interactions/CHANGELOG.md similarity index 100% rename from nyxx.interactions/CHANGELOG.md rename to nyxx_interactions/CHANGELOG.md diff --git a/nyxx.interactions/LICENSE b/nyxx_interactions/LICENSE similarity index 100% rename from nyxx.interactions/LICENSE rename to nyxx_interactions/LICENSE diff --git a/nyxx.interactions/README.md b/nyxx_interactions/README.md similarity index 99% rename from nyxx.interactions/README.md rename to nyxx_interactions/README.md index a6d02652..4c1f77bf 100644 --- a/nyxx.interactions/README.md +++ b/nyxx_interactions/README.md @@ -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/) diff --git a/nyxx.interactions/analysis_options.yaml b/nyxx_interactions/analysis_options.yaml similarity index 100% rename from nyxx.interactions/analysis_options.yaml rename to nyxx_interactions/analysis_options.yaml diff --git a/nyxx_interactions/example/interactions-basic.dart b/nyxx_interactions/example/interactions-basic.dart new file mode 100644 index 00000000..e0d243ef --- /dev/null +++ b/nyxx_interactions/example/interactions-basic.dart @@ -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")); + // } + // }); + // }); +} diff --git a/nyxx.interactions/lib/interactions.dart b/nyxx_interactions/lib/interactions.dart similarity index 67% rename from nyxx.interactions/lib/interactions.dart rename to nyxx_interactions/lib/interactions.dart index 83e07c38..dd2c628b 100644 --- a/nyxx.interactions/lib/interactions.dart +++ b/nyxx_interactions/lib/interactions.dart @@ -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"; diff --git a/nyxx_interactions/lib/src/Interactions.dart b/nyxx_interactions/lib/src/Interactions.dart new file mode 100644 index 00000000..037d1406 --- /dev/null +++ b/nyxx_interactions/lib/src/Interactions.dart @@ -0,0 +1,152 @@ +part of nyxx_interactions; + +typedef SlashCommandHandlder = FutureOr 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 = []; + final _commands = []; + final _commandHandlers = {}; + + /// Emitted when a slash command is sent. + late final Stream onSlashCommand; + + /// Emitted when a slash command is created by the user. + late final Stream 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)); + } + }); + }); + } + + void syncOnReady() { + this._client.onReady.listen((_) async { + await this.sync(); + }); + } + + /// Syncs command builders with discord + Future sync() async { + final commandPartition = _partition(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 builders) { + final registeredSlashCommands = (response.jsonBody as List).map((e) => SlashCommand._new(e as Map, 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!; + } + } +} diff --git a/nyxx.interactions/lib/src/models/commandArgs/ArgChoice.dart b/nyxx_interactions/lib/src/builders/ArgChoiceBuilder.dart similarity index 50% rename from nyxx.interactions/lib/src/models/commandArgs/ArgChoice.dart rename to nyxx_interactions/lib/src/builders/ArgChoiceBuilder.dart index a054299a..1ecc3457 100644 --- a/nyxx.interactions/lib/src/models/commandArgs/ArgChoice.dart +++ b/nyxx_interactions/lib/src/builders/ArgChoiceBuilder.dart @@ -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 _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 _build() => { "name": this.name, "value": this.value }; +} diff --git a/nyxx_interactions/lib/src/builders/CommandOptionBuilder.dart b/nyxx_interactions/lib/src/builders/CommandOptionBuilder.dart new file mode 100644 index 00000000..e778b92e --- /dev/null +++ b/nyxx_interactions/lib/src/builders/CommandOptionBuilder.dart @@ -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? choices; + + /// If the option is a subcommand or subcommand group type, this nested options will be the parameters + List? 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 _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; + } +} diff --git a/nyxx_interactions/lib/src/builders/SlashCommandBuilder.dart b/nyxx_interactions/lib/src/builders/SlashCommandBuilder.dart new file mode 100644 index 00000000..22d380d4 --- /dev/null +++ b/nyxx_interactions/lib/src/builders/SlashCommandBuilder.dart @@ -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 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 _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; +} diff --git a/nyxx.interactions/lib/src/events/InteractionEvent.dart b/nyxx_interactions/lib/src/events/InteractionEvent.dart similarity index 94% rename from nyxx.interactions/lib/src/events/InteractionEvent.dart rename to nyxx_interactions/lib/src/events/InteractionEvent.dart index 99307497..044023cc 100644 --- a/nyxx.interactions/lib/src/events/InteractionEvent.dart +++ b/nyxx_interactions/lib/src/events/InteractionEvent.dart @@ -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 rawJson) { this.interaction = Interaction._new(this._client, rawJson); diff --git a/nyxx.interactions/lib/src/exceptions/AlreadyResponded.dart b/nyxx_interactions/lib/src/exceptions/AlreadyResponded.dart similarity index 100% rename from nyxx.interactions/lib/src/exceptions/AlreadyResponded.dart rename to nyxx_interactions/lib/src/exceptions/AlreadyResponded.dart diff --git a/nyxx.interactions/lib/src/exceptions/InteractionExpired.dart b/nyxx_interactions/lib/src/exceptions/InteractionExpired.dart similarity index 100% rename from nyxx.interactions/lib/src/exceptions/InteractionExpired.dart rename to nyxx_interactions/lib/src/exceptions/InteractionExpired.dart diff --git a/nyxx.interactions/lib/src/internal/_EventController.dart b/nyxx_interactions/lib/src/internal/_EventController.dart similarity index 100% rename from nyxx.interactions/lib/src/internal/_EventController.dart rename to nyxx_interactions/lib/src/internal/_EventController.dart diff --git a/nyxx_interactions/lib/src/internal/utils.dart b/nyxx_interactions/lib/src/internal/utils.dart new file mode 100644 index 00000000..f6a64861 --- /dev/null +++ b/nyxx_interactions/lib/src/internal/utils.dart @@ -0,0 +1,50 @@ +part of nyxx_interactions; + +Iterable> _partition(Iterable list, bool Function(T) predicate) { + final matches = []; + final nonMatches = []; + + 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> _groupSlashCommandBuilders(Iterable commands) { + final commandsMap = >{}; + + for(final slashCommand in commands) { + final id = slashCommand.guild!; + + if (commandsMap.containsKey(id)) { + commandsMap[id]!.add(slashCommand); + continue; + } + + commandsMap[id] = [slashCommand]; + } + + return commandsMap; +} diff --git a/nyxx_interactions/lib/src/models/ArgChoice.dart b/nyxx_interactions/lib/src/models/ArgChoice.dart new file mode 100644 index 00000000..64ca4839 --- /dev/null +++ b/nyxx_interactions/lib/src/models/ArgChoice.dart @@ -0,0 +1,12 @@ +part of nyxx_interactions; + +class ArgChoice { + late final String name; + + late final dynamic value; + + ArgChoice._new(Map raw) { + this.name = raw["name"] as String; + this.value = raw["value"]; + } +} diff --git a/nyxx_interactions/lib/src/models/CommandOption.dart b/nyxx_interactions/lib/src/models/CommandOption.dart new file mode 100644 index 00000000..73960510 --- /dev/null +++ b/nyxx_interactions/lib/src/models/CommandOption.dart @@ -0,0 +1,73 @@ +part of nyxx_interactions; + +/// The type that a user should input for a [CommandOptionBuilder] +class CommandOptionType extends IEnum { + /// 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 choices; + + /// If the option is a subcommand or subcommand group type, this nested options will be the parameters + late final List options; + + CommandOption._new(Map 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) + ]; + + this.options = [ + if (raw["options"] != null) + for(final optionRaw in raw["options"]) + CommandOption._new(optionRaw as Map) + ]; + } +} diff --git a/nyxx.interactions/lib/src/models/Interaction.dart b/nyxx_interactions/lib/src/models/Interaction.dart similarity index 88% rename from nyxx.interactions/lib/src/models/Interaction.dart rename to nyxx_interactions/lib/src/models/Interaction.dart index e42fdbfb..a1a61054 100644 --- a/nyxx.interactions/lib/src/models/Interaction.dart +++ b/nyxx_interactions/lib/src/models/Interaction.dart @@ -9,7 +9,7 @@ class Interaction extends SnowflakeEntity { late final int type; /// The guild the command was sent in. - late final Cacheable guild; + late final Cacheable? guild; /// The channel the command was sent in. late final Cacheable channel; @@ -35,10 +35,14 @@ class Interaction extends SnowflakeEntity { Interaction._new(this._client, Map 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, diff --git a/nyxx.interactions/lib/src/models/InteractionOption.dart b/nyxx_interactions/lib/src/models/InteractionOption.dart similarity index 78% rename from nyxx.interactions/lib/src/models/InteractionOption.dart rename to nyxx_interactions/lib/src/models/InteractionOption.dart index a3111465..d67660c5 100644 --- a/nyxx.interactions/lib/src/models/InteractionOption.dart +++ b/nyxx_interactions/lib/src/models/InteractionOption.dart @@ -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 args; /// Option choices - late final Iterable choices; + late final Iterable choices; InteractionOption._new(Map 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).map((e) => InteractionOption._new(e as Map)); } if (raw["choices"] != null) { - this.choices = (raw["options"] as List>).map((e) => ArgChoice(e["name"] as String, e["value"])); + this.choices = (raw["options"] as List>).map((e) => ArgChoiceBuilder(e["name"] as String, e["value"])); } } } diff --git a/nyxx_interactions/lib/src/models/SlashCommand.dart b/nyxx_interactions/lib/src/models/SlashCommand.dart new file mode 100644 index 00000000..df18f565 --- /dev/null +++ b/nyxx_interactions/lib/src/models/SlashCommand.dart @@ -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 options; + + SlashCommand._new(Map 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) + ]; + } +} diff --git a/nyxx.interactions/pubspec.yaml b/nyxx_interactions/pubspec.yaml similarity index 100% rename from nyxx.interactions/pubspec.yaml rename to nyxx_interactions/pubspec.yaml