From 661982c319c94fdd5c2313d6c9023cf7d4280992 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 29 Jul 2022 23:22:05 +0200 Subject: [PATCH] Release 4.3.0 (#56) * feature: Locales (#48) * Locales * Fix and add deprecated notice * Fix template * Format * cov: unit tests * Add enum locale, bump to Dart 2.17 * fix: Links in pubspec * fix: Query param * fix: `Endpoints@fetchGuildCommand()` * suggestions: `withLocales` to `true` * fix: queryParams * fix: typos * fix: Remove `withLocales` param Not used by discord, I'm just dumb * feat: Add deserialize onto `Locale` * fix: Deserialize `RawApiMap` into full `Locale` + add unit tests for localizations. * feat: Add localizations on receivied options. * Differentiate command handlers per guild * Fixup workflows * Format code * Fix interaction acknowledgement 3 second timeout acknowledged interaction tokens were expiring in 3 seconds rather than the intended 15 minutes * Fix typo * Revert #37 * Remove trailing whitespace in docs * Fix crash in Locale * Fix unit tests * Update to nyxx 4.0.0 (#54) * Release 4.3.0 (#55) Co-authored-by: Rapougnac <74512338+Rapougnac@users.noreply.github.com> Co-authored-by: Abitofevrything Co-authored-by: Nicholas Shrefler <16249086+NDSo@users.noreply.github.com> Co-authored-by: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> --- .github/workflows/unit_tests.yml | 2 +- CHANGELOG.md | 7 + Makefile | 2 +- example/buttons_and_dropdowns.dart | 8 +- example/example.dart | 11 +- lib/nyxx_interactions.dart | 3 +- lib/src/builders/command_option_builder.dart | 18 +- .../builders/command_permission_builder.dart | 6 +- lib/src/builders/slash_command_builder.dart | 54 ++- lib/src/events/interaction_event.dart | 10 +- lib/src/interactions.dart | 18 +- lib/src/internal/interaction_endpoints.dart | 367 +++++++++++++----- lib/src/internal/utils.dart | 5 +- lib/src/models/command_option.dart | 17 +- lib/src/models/locale.dart | 45 +++ lib/src/models/slash_command.dart | 18 +- lib/src/models/slash_command_permission.dart | 2 +- pubspec.yaml | 12 +- test/unit/command_option_builder.dart | 20 +- test/unit/slash_command_builder.dart | 24 +- 20 files changed, 497 insertions(+), 152 deletions(-) create mode 100644 lib/src/models/locale.dart diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index e2521a3..bcbf3b3 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -89,7 +89,7 @@ jobs: run: dart run test --coverage="coverage" test/unit/** - name: Format coverage - run: dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.packages --report-on=lib + run: dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.dart_tool/package_config.json --report-on=lib - name: Generate coverage run: genhtml coverage/coverage.lcov -o coverage/coverage_gen diff --git a/CHANGELOG.md b/CHANGELOG.md index 13590f5..f7e90cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 4.3.0 +__29.07.2022__ + +- feature: Update to nyxx 4.0.0 (#54) +- feature: Locales (#48) +- feature: Differentiate command handlers per guild + ## 4.2.1 __02.05.2022__ diff --git a/Makefile b/Makefile index 50bc561..399b169 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ unit-tests: ## Run unit tests with coverage .PHONY: coverage-format coverage-format: ## Format dart coverage output to lcov - dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.packages --report-on=lib + dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.dart_tool/package_config.json --report-on=lib .PHONY: coverage-gen-html coverage-gen-html: ## Generate html coverage from lcov data diff --git a/example/buttons_and_dropdowns.dart b/example/buttons_and_dropdowns.dart index d19a8a0..fe936ad 100644 --- a/example/buttons_and_dropdowns.dart +++ b/example/buttons_and_dropdowns.dart @@ -43,9 +43,7 @@ Future buttonHandler(IButtonInteractionEvent event) async { await event.acknowledge(); // ack the interaction so we can send response later // Send followup to button click with id of button - await event.sendFollowup(MessageBuilder.content( - "Button pressed with id: ${event.interaction.customId}") - ); + await event.sendFollowup(MessageBuilder.content("Button pressed with id: ${event.interaction.customId}")); } // Handling multiselect events is no different from handling button. @@ -55,9 +53,7 @@ Future multiselectHandlerHandler(IMultiselectInteractionEvent event) async await event.acknowledge(); // ack the interaction so we can send response later // Send followup to button click with id of button - await event.sendFollowup(MessageBuilder.content( - "Option chosen with values: ${event.interaction.values}") - ); + await event.sendFollowup(MessageBuilder.content("Option chosen with values: ${event.interaction.values}")); } void main() { diff --git a/example/example.dart b/example/example.dart index cec1ae9..45974f9 100644 --- a/example/example.dart +++ b/example/example.dart @@ -9,10 +9,13 @@ void main() { ..connect(); IInteractions.create(WebsocketInteractionBackend(bot)) - ..registerSlashCommand( - SlashCommandBuilder("itest", "This is test command", [ + ..registerSlashCommand(SlashCommandBuilder( + "itest", + "This is test command", + [ CommandOptionBuilder(CommandOptionType.subCommand, "subtest", "This is sub test") ..registerHandler((event) => event.respond(MessageBuilder.content("This is example command"))) - ], guild: 302360552993456135.toSnowflake()) - )..syncOnReady(); + ], + guild: 302360552993456135.toSnowflake())) + ..syncOnReady(); } diff --git a/lib/nyxx_interactions.dart b/lib/nyxx_interactions.dart index 3fb5bb9..dbca9fc 100644 --- a/lib/nyxx_interactions.dart +++ b/lib/nyxx_interactions.dart @@ -35,7 +35,7 @@ export 'src/internal/sync/commands_sync.dart' show ICommandsSync; export 'src/internal/sync/lock_file_command_sync.dart' show LockFileCommandSync; export 'src/internal/sync/manual_command_sync.dart' show ManualCommandSync; export 'src/internal/event_controller.dart' show IEventController; -export 'src/internal/interaction_endpoints.dart' show IInteractionsEndpoints; +export 'src/internal/interaction_endpoints.dart' show IInteractionsEndpoints, InteractionRouteParts; export 'src/internal/utils.dart' show slashCommandNameRegex; export 'src/models/arg_choice.dart' show IArgChoice; export 'src/models/command_option.dart' show ICommandOption, CommandOptionType; @@ -46,6 +46,7 @@ export 'src/models/interaction_option.dart' show IInteractionOption; export 'src/models/slash_command_permission.dart' show ISlashCommandPermissionOverride, ISlashCommandPermissionOverrides, SlashCommandPermissionType; export 'src/models/slash_command.dart' show ISlashCommand; export 'src/models/slash_command_type.dart' show SlashCommandType; +export 'src/models/locale.dart' show Locale; export 'src/interactions.dart' show IInteractions; export 'src/typedefs.dart' show AutocompleteInteractionHandler, ButtonInteractionHandler, MultiselectInteractionHandler, SlashCommandHandler; diff --git a/lib/src/builders/command_option_builder.dart b/lib/src/builders/command_option_builder.dart index d20fc7e..d5e1b62 100644 --- a/lib/src/builders/command_option_builder.dart +++ b/lib/src/builders/command_option_builder.dart @@ -2,7 +2,9 @@ import 'package:nyxx/nyxx.dart'; import 'package:nyxx_interactions/src/builders/arg_choice_builder.dart'; import 'package:nyxx_interactions/src/models/command_option.dart'; +import 'package:nyxx_interactions/src/models/locale.dart'; import 'package:nyxx_interactions/src/typedefs.dart'; +import 'package:nyxx_interactions/src/builders/slash_command_builder.dart'; /// An argument for a [SlashCommandBuilder]. class CommandOptionBuilder extends Builder { @@ -22,16 +24,24 @@ class CommandOptionBuilder extends Builder { /// The name of your argument / sub-group. final String name; + /// The localizations name of your argument / sub-group. + /// See [SlashCommandBuilder.localizationsName] for more information. + final Map? localizationsName; + /// The description of your argument / sub-group. final String description; - /// If this should be the fist required option the user picks + /// The localizations description of your argument / sub-group. + /// See [SlashCommandBuilder.localizationsDescription] for more information. + final Map? localizationsDescription; + + /// If this should be the first 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 + /// Choices for [CommandOptionType.string], [CommandOptionType.integer] and [CommandOptionType.number] 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 @@ -66,6 +76,8 @@ class CommandOptionBuilder extends Builder { this.autoComplete = false, this.min, this.max, + this.localizationsName, + this.localizationsDescription, }); /// Registers handler for subcommand @@ -98,6 +110,8 @@ class CommandOptionBuilder extends Builder { if (channelTypes != null && type == CommandOptionType.channel) "channel_types": channelTypes!.map((e) => e.value).toList(), if (min != null) "min_value": min, if (max != null) "max_value": max, + if (localizationsName != null) "name_localizations": localizationsName!.map((k, v) => MapEntry(k.toString(), v)), + if (localizationsDescription != null) "description_localizations": localizationsDescription!.map((k, v) => MapEntry(k.toString(), v)), "autocomplete": autoComplete, }; } diff --git a/lib/src/builders/command_permission_builder.dart b/lib/src/builders/command_permission_builder.dart index 8178d9b..c069f25 100644 --- a/lib/src/builders/command_permission_builder.dart +++ b/lib/src/builders/command_permission_builder.dart @@ -1,7 +1,7 @@ import 'package:nyxx/nyxx.dart'; /// Used to define permissions for a particular command. -@Deprecated('Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiresPermissions instead') +@Deprecated('Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiredPermissions instead') abstract class CommandPermissionBuilderAbstract extends Builder { int get type; @@ -21,7 +21,7 @@ abstract class CommandPermissionBuilderAbstract extends Builder { } /// A permission for a single role that can be used in [SlashCommandBuilder] -@Deprecated('Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiresPermissions instead') +@Deprecated('Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiredPermissions instead') class RoleCommandPermissionBuilder extends CommandPermissionBuilderAbstract { @override late final int type = 1; @@ -34,7 +34,7 @@ class RoleCommandPermissionBuilder extends CommandPermissionBuilderAbstract { } /// A permission for a single user that can be used in [SlashCommandBuilder] -@Deprecated('Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiresPermissions instead') +@Deprecated('Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiredPermissions instead') class UserCommandPermissionBuilder extends CommandPermissionBuilderAbstract { @override late final int type = 2; diff --git a/lib/src/builders/slash_command_builder.dart b/lib/src/builders/slash_command_builder.dart index fa91579..9c6c5e6 100644 --- a/lib/src/builders/slash_command_builder.dart +++ b/lib/src/builders/slash_command_builder.dart @@ -2,14 +2,14 @@ import 'package:nyxx/nyxx.dart'; import 'package:nyxx_interactions/src/builders/command_option_builder.dart'; import 'package:nyxx_interactions/src/builders/command_permission_builder.dart'; - +import 'package:nyxx_interactions/src/models/locale.dart'; import 'package:nyxx_interactions/src/models/slash_command_type.dart'; import 'package:nyxx_interactions/src/models/command_option.dart'; import 'package:nyxx_interactions/src/interactions.dart'; import 'package:nyxx_interactions/src/internal/utils.dart'; import 'package:nyxx_interactions/src/typedefs.dart'; -/// A slash command, can only be instantiated through a method on [Interactions] +/// A slash command, can only be instantiated through a method on [IInteractions] class SlashCommandBuilder extends Builder { /// The commands ID that is defined on registration and used for permission syncing. late final Snowflake _id; @@ -17,11 +17,45 @@ class SlashCommandBuilder extends Builder { /// Command name to be shown to the user in the Slash Command UI final String name; + /// The command names to be shown to the user in the Slash Command UI by specified locales. + /// See the [available locales](https://discord.com/developers/docs/reference#locales) for a list of available locales. + /// The key is the locale and the value is the name of the command in that locale. + /// Values follow the same constraints as [name] (`^[\w-]{1,32}$`). + /// + /// An example: + /// {@template slashcommand.builder.example} + /// ```dart + /// final scb = SlashCommandBuilder( + /// 'hello', + /// 'Hello World!', + /// [], + /// localizationsName: { + /// Locale.french: 'salut', + /// Locale.german: 'hallo', + /// }, + /// localizationsDescription: { + /// Locale.french: 'Salut le monde !', + /// Locale.german: 'Hallo Welt!', + /// }, + /// ); + /// ``` + /// {@endtemplate} + final Map? localizationsName; + /// Command description shown to the user in the Slash Command UI final String? description; + /// The command descriptions to be shown to the user in the Slash Command UI by specified locales. + /// See the [available locales](https://discord.com/developers/docs/reference#locales) for a list of available locales. + /// The key is the locale and the value is the description of the command in that locale. + /// Values follow the same constraints as [description]. + /// + /// An example: + /// {@macro slashcommand.builder.example} + final Map? localizationsDescription; + /// If people can use the command by default or if they need permissions to use it. - @Deprecated('Use canBeUsedInDm and requiresPermissions instead') + @Deprecated('Use canBeUsedInDm and requiredPermissions instead') final bool defaultPermissions; /// The guild that the slash Command is registered in. This can be null if its a global command. @@ -31,7 +65,7 @@ class SlashCommandBuilder extends Builder { List options; /// Permission overrides for the command - @Deprecated('Use canBeUsedInDm and requiresPermissions instead') + @Deprecated('Use canBeUsedInDm and requiredPermissions instead') List? permissions; /// Target of slash command if different that SlashCommandTarget.chat - slash command will @@ -50,7 +84,7 @@ class SlashCommandBuilder extends Builder { /// operator, they will be allowed to execute the command. int? requiredPermissions; - /// A slash command, can only be instantiated through a method on [Interactions] + /// A slash command, can only be instantiated through a method on [IInteractions] SlashCommandBuilder( this.name, this.description, @@ -59,8 +93,10 @@ class SlashCommandBuilder extends Builder { this.requiredPermissions, this.guild, this.type = SlashCommandType.chat, - this.defaultPermissions = true, - this.permissions, + @Deprecated('Use canBeUsedInDm and requiredPermissions instead') this.defaultPermissions = true, + @Deprecated('Use canBeUsedInDm and requiredPermissions instead') this.permissions, + this.localizationsName, + this.localizationsDescription, }) { if (!slashCommandNameRegex.hasMatch(name)) { throw ArgumentError("Command name has to match regex: ${slashCommandNameRegex.pattern}"); @@ -83,6 +119,8 @@ class SlashCommandBuilder extends Builder { "type": type.value, "dm_permission": canBeUsedInDm, if (requiredPermissions != null) "default_member_permissions": requiredPermissions.toString(), + if (localizationsName != null) "name_localizations": localizationsName!.map((k, v) => MapEntry(k.toString(), v)), + if (localizationsDescription != null) "description_localizations": localizationsDescription!.map((k, v) => MapEntry(k.toString(), v)), "default_permission": defaultPermissions, }; @@ -91,7 +129,7 @@ class SlashCommandBuilder extends Builder { Snowflake get id => _id; /// Register a permission - @Deprecated('Use canBeUsedInDm and requiresPermissions instead') + @Deprecated('Use canBeUsedInDm and requiredPermissions instead') void addPermission(CommandPermissionBuilderAbstract permission) { permissions ??= []; diff --git a/lib/src/events/interaction_event.dart b/lib/src/events/interaction_event.dart index 00b44c1..fa4ad56 100644 --- a/lib/src/events/interaction_event.dart +++ b/lib/src/events/interaction_event.dart @@ -13,7 +13,7 @@ import 'package:nyxx_interactions/src/exceptions/already_responded.dart'; import 'package:nyxx/nyxx.dart'; abstract class IInteractionEvent { - /// Reference to [Nyxx] + /// Reference to [INyxx] INyxx get client; /// Reference to [Interactions] @@ -27,7 +27,7 @@ abstract class IInteractionEvent { } abstract class InteractionEventAbstract implements IInteractionEvent { - /// Reference to [Nyxx] + /// Reference to [INyxx] @override INyxx get client => interactions.client; @@ -41,7 +41,7 @@ abstract class InteractionEventAbstract implements IInte /// The DateTime the interaction was received by the Nyxx Client. @override - DateTime get receivedAt => interaction.id.timestamp; + final DateTime receivedAt = DateTime.now(); final Logger logger = Logger("Interaction Event"); @@ -204,7 +204,7 @@ abstract class InteractionEventWithAcknowledge extends I final now = DateTime.now(); if (_hasAcked && now.isAfter(receivedAt.add(const Duration(minutes: 15)))) { return Future.error(InteractionExpiredError.fifteenMins()); - } else if (now.isAfter(receivedAt.add(const Duration(seconds: 3)))) { + } else if (!_hasAcked && now.isAfter(receivedAt.add(const Duration(seconds: 3)))) { return Future.error(InteractionExpiredError.threeSecs()); } @@ -222,7 +222,7 @@ abstract class InteractionEventWithAcknowledge extends I _hasAcked = true; } - /// Returns [Message] object of original interaction response + /// Returns [IMessage] object of original interaction response @override Future getOriginalResponse() async => interactions.interactionsEndpoints.fetchOriginalResponse(interaction.token, client.appId, interaction.id.toString()); diff --git a/lib/src/interactions.dart b/lib/src/interactions.dart index 7b28397..3edda88 100644 --- a/lib/src/interactions.dart +++ b/lib/src/interactions.dart @@ -66,15 +66,15 @@ abstract class IInteractions { Future deleteGuildCommands(List guildIds); /// Fetches all global bots command - Stream fetchGlobalCommands(); + Stream fetchGlobalCommands({bool withLocales = true}); /// Fetches all guild commands for given guild - Stream fetchGuildCommands(Snowflake guildId); + Stream fetchGuildCommands(Snowflake guildId, {bool withLocales = true}); /// Returns the global overrides for commands in a guild. Cacheable getGlobalOverridesInGuild(Snowflake guildId); - static IInteractions create(InteractionBackend backend) => Interactions(backend); + factory IInteractions.create(InteractionBackend backend) => Interactions(backend); } /// Interaction extension for Nyxx. Allows use of: Slash Commands. @@ -207,7 +207,7 @@ class Interactions implements IInteractions { if (entry.value.any((element) => element.permissions?.isNotEmpty ?? false)) { _logger.warning( - 'Using deprecated permissions endpoint. To fix, use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiresPermissions' + 'Using deprecated permissions endpoint. To fix, use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiredPermissions' ' instead of SlashCommandBuilder.permissions', ); await interactionsEndpoints.bulkOverrideGuildCommandsPermissions(client.appId, entry.key, entry.value); @@ -309,11 +309,12 @@ class Interactions implements IInteractions { /// Fetches all global bots command @override - Stream fetchGlobalCommands() => interactionsEndpoints.fetchGlobalCommands(client.appId); + Stream fetchGlobalCommands({bool withLocales = true}) => interactionsEndpoints.fetchGlobalCommands(client.appId, withLocales: withLocales); /// Fetches all guild commands for given guild @override - Stream fetchGuildCommands(Snowflake guildId) => interactionsEndpoints.fetchGuildCommands(client.appId, guildId); + Stream fetchGuildCommands(Snowflake guildId, {bool withLocales = true}) => + interactionsEndpoints.fetchGuildCommands(client.appId, guildId, withLocales: withLocales); @override Cacheable getGlobalOverridesInGuild(Snowflake guildId) => @@ -336,7 +337,10 @@ class Interactions implements IInteractions { } void _assignCommandToHandler(SlashCommandBuilder builder) { - final commandHashPrefix = builder.name; + String commandHashPrefix = builder.name; + if (builder.guild != null) { + commandHashPrefix = '${builder.guild}/$commandHashPrefix'; + } var allowRootHandler = true; diff --git a/lib/src/internal/interaction_endpoints.dart b/lib/src/internal/interaction_endpoints.dart index 0e32c3d..694e7a6 100644 --- a/lib/src/internal/interaction_endpoints.dart +++ b/lib/src/internal/interaction_endpoints.dart @@ -46,7 +46,7 @@ abstract class IInteractionsEndpoints { Future editFollowup(String token, Snowflake applicationId, Snowflake messageId, MessageBuilder builder); /// Fetches global commands of application - Stream fetchGlobalCommands(Snowflake applicationId); + Stream fetchGlobalCommands(Snowflake applicationId, {bool withLocales = true}); /// Fetches global command with given [commandId] Future fetchGlobalCommand(Snowflake applicationId, Snowflake commandId); @@ -61,7 +61,7 @@ abstract class IInteractionsEndpoints { Stream bulkOverrideGlobalCommands(Snowflake applicationId, Iterable builders); /// Fetches all commands for given [guildId] - Stream fetchGuildCommands(Snowflake applicationId, Snowflake guildId); + Stream fetchGuildCommands(Snowflake applicationId, Snowflake guildId, {bool withLocales = true}); /// Fetches single guild command with given [commandId] Future fetchGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId); @@ -77,7 +77,7 @@ abstract class IInteractionsEndpoints { /// Overrides permissions for guild commands @Deprecated("This endpoint requires OAuth2 authentication, which nyxx_interactions doesn't support." - " Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiresPermissions instead.") + " Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiredPermissions instead.") Future bulkOverrideGuildCommandsPermissions(Snowflake applicationId, Snowflake guildId, Iterable builders); /// Responds to autocomplete interaction @@ -90,6 +90,21 @@ abstract class IInteractionsEndpoints { Future> fetchPermissionOverrides(Snowflake guildId); } +extension InteractionRouteParts on IHttpRoute { + /// Adds the [`interactions`](https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response) part to this + /// [IHttpRoute]. + void interactions({String? id, String? token}) => add(HttpRoutePart('interactions', [ + if (id != null) HttpRouteParam(id), + if (token != null) HttpRouteParam(token), + ])); + + /// Adds the [`callback`](https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response) part to this [IHttpRoute]. + void callback() => add(HttpRoutePart('callback')); + + /// Adds the [`commands`](https://discord.com/developers/docs/interactions/application-commands#get-global-application-commands) part to this [IHttpRoute]. + void commands({String? id}) => add(HttpRoutePart('commands', [if (id != null) HttpRouteParam(id)])); +} + class InteractionsEndpoints implements IInteractionsEndpoints { final INyxx _client; final Interactions _interactions; @@ -100,13 +115,18 @@ class InteractionsEndpoints implements IInteractionsEndpoints { @override Future acknowledge(String token, String interactionId, bool hidden, int opCode) async { - final url = "/interactions/$interactionId/$token/callback"; - final response = await _client.httpEndpoints.sendRawRequest(url, "POST", body: { - "type": opCode, - "data": { - if (hidden) "flags": 1 << 6, - } - }); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..interactions(id: interactionId, token: token) + ..callback(), + "POST", + body: { + "type": opCode, + "data": { + if (hidden) "flags": 1 << 6, + } + }, + ); if (response is IHttpResponseError) { return Future.error(response); @@ -114,15 +134,22 @@ class InteractionsEndpoints implements IInteractionsEndpoints { } @override - Future deleteFollowup(String token, Snowflake applicationId, Snowflake messageId) => - _client.httpEndpoints.sendRawRequest("webhooks/$applicationId/$token/messages/$messageId", "DELETE"); + Future deleteFollowup(String token, Snowflake applicationId, Snowflake messageId) => _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..webhooks(id: applicationId.id.toString(), token: token) + ..messages(id: messageId.id.toString()), + "DELETE", + ); @override Future deleteOriginalResponse(String token, Snowflake applicationId, String interactionId) async { - final url = "/webhooks/$applicationId/$token/messages/@original"; - const method = "DELETE"; + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..webhooks(id: applicationId.id.toString(), token: token) + ..messages(id: '@original'), + "DELETE", + ); - final response = await _client.httpEndpoints.sendRawRequest(url, method); if (response is IHttpResponseError) { return Future.error(response); } @@ -130,44 +157,66 @@ class InteractionsEndpoints implements IInteractionsEndpoints { @override Future editFollowup(String token, Snowflake applicationId, Snowflake messageId, MessageBuilder builder) async { - final url = "/webhooks/$applicationId/$token/messages/$messageId"; final body = builder.build(_client.options.allowedMentions); - final response = await _client.httpEndpoints.sendRawRequest(url, "PATCH", body: body); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..webhooks(id: applicationId.id.toString(), token: token) + ..messages(id: messageId.id.toString()), + "PATCH", + body: body, + ); + if (response is IHttpResponseError) { return Future.error(response); } - return Message(_client, (response as IHttpResponseSucess).jsonBody as RawApiMap); + return Message(_client, (response as IHttpResponseSuccess).jsonBody as RawApiMap); } @override Future editOriginalResponse(String token, Snowflake applicationId, MessageBuilder builder) async { - final response = await _client.httpEndpoints - .sendRawRequest("/webhooks/$applicationId/$token/messages/@original", "PATCH", body: builder.build(_client.options.allowedMentions)); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..webhooks(id: applicationId.id.toString(), token: token) + ..messages(id: '@original'), + "PATCH", + body: builder.build(_client.options.allowedMentions), + ); if (response is IHttpResponseError) { return Future.error(response); } - return Message(_client, (response as IHttpResponseSucess).jsonBody as RawApiMap); + return Message(_client, (response as IHttpResponseSuccess).jsonBody as RawApiMap); } @override Future fetchOriginalResponse(String token, Snowflake applicationId, String interactionId) async { - final response = await _client.httpEndpoints.sendRawRequest("/webhooks/$applicationId/$token/messages/@original", "GET"); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..webhooks(id: applicationId.id.toString(), token: token) + ..messages(id: '@original'), + "GET", + ); if (response is IHttpResponseError) { return Future.error(response); } - return Message(_client, (response as IHttpResponseSucess).jsonBody as RawApiMap); + return Message(_client, (response as IHttpResponseSuccess).jsonBody as RawApiMap); } @override Future respondEditOriginal(String token, Snowflake applicationId, MessageBuilder builder, bool hidden) async { - final response = await _client.httpEndpoints.sendRawRequest("/webhooks/$applicationId/$token/messages/@original", "PATCH", - body: {if (hidden) "flags": 1 << 6, ...builder.build(_client.options.allowedMentions)}, files: builder.files ?? []); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..webhooks(id: applicationId.id.toString(), token: token) + ..messages(id: '@original'), + "PATCH", + body: {if (hidden) "flags": 1 << 6, ...builder.build(_client.options.allowedMentions)}, + files: builder.files ?? [], + ); if (response is IHttpResponseError) { return Future.error(response); @@ -176,12 +225,20 @@ class InteractionsEndpoints implements IInteractionsEndpoints { @override Future respondCreateResponse(String token, String interactionId, MessageBuilder builder, bool hidden, int respondOpCode) async { - final response = await _client.httpEndpoints.sendRawRequest("/interactions/${interactionId.toString()}/$token/callback", "POST", - body: { - "type": respondOpCode, - "data": {if (hidden) "flags": 1 << 6, ...builder.build(_client.options.allowedMentions)}, + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..interactions(id: interactionId, token: token) + ..callback(), + "POST", + body: { + "type": respondOpCode, + "data": { + if (hidden) "flags": 1 << 6, + ...builder.build(_client.options.allowedMentions), }, - files: builder.files ?? []); + }, + files: builder.files ?? [], + ); if (response is IHttpResponseError) { return Future.error(response); @@ -190,138 +247,198 @@ class InteractionsEndpoints implements IInteractionsEndpoints { @override Future sendFollowup(String token, Snowflake applicationId, MessageBuilder builder, {bool hidden = false}) async { - final response = await _client.httpEndpoints.sendRawRequest("/webhooks/$applicationId/$token", "POST", - body: { - ...builder.build(_client.options.allowedMentions), - if (hidden) 'flags': 1 << 6, - }, - files: builder.files ?? []); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute()..webhooks(id: applicationId.id.toString(), token: token), + "POST", + body: { + ...builder.build(_client.options.allowedMentions), + if (hidden) 'flags': 1 << 6, + }, + files: builder.files ?? [], + ); if (response is IHttpResponseError) { return Future.error(response); } - return Message(_client, (response as IHttpResponseSucess).jsonBody as RawApiMap); + return Message(_client, (response as IHttpResponseSuccess).jsonBody as RawApiMap); } @override Stream bulkOverrideGlobalCommands(Snowflake applicationId, Iterable builders) async* { - final response = await _client.httpEndpoints - .sendRawRequest("/applications/$applicationId/commands", "PUT", body: [for (final builder in builders) builder.build()], auth: true); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..commands(), + "PUT", + body: [for (final builder in builders) builder.build()], + auth: true, + ); if (response is IHttpResponseError) { yield* Stream.error(response); } - for (final rawRes in (response as IHttpResponseSucess).jsonBody as List) { + for (final rawRes in (response as IHttpResponseSuccess).jsonBody as List) { yield SlashCommand(rawRes as RawApiMap, _interactions); } } @override Stream bulkOverrideGuildCommands(Snowflake applicationId, Snowflake guildId, Iterable builders) async* { - final response = await _client.httpEndpoints - .sendRawRequest("/applications/$applicationId/guilds/$guildId/commands", "PUT", body: [for (final builder in builders) builder.build()], auth: true); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..guilds(id: guildId.id.toString()) + ..commands(), + "PUT", + body: [for (final builder in builders) builder.build()], + auth: true, + ); + if (response is IHttpResponseError) { yield* Stream.error(response); } - for (final rawRes in (response as IHttpResponseSucess).jsonBody as List) { + for (final rawRes in (response as IHttpResponseSuccess).jsonBody as List) { yield SlashCommand(rawRes as RawApiMap, _interactions); } } @override - Future deleteGlobalCommand(Snowflake applicationId, Snowflake commandId) async { - final response = await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/commands/$commandId", "DELETE", auth: true); - - if (response is IHttpResponseSucess) { - return Future.error(response); - } - } + Future deleteGlobalCommand(Snowflake applicationId, Snowflake commandId) => _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..commands(id: commandId.id.toString()), + "DELETE", + auth: true, + ); @override - Future deleteGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId) async { - final response = await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/guilds/$guildId/commands/$commandId", "DELETE", auth: true); - - if (response is IHttpResponseSucess) { - return Future.error(response); - } - } + Future deleteGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId) => _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..guilds(id: guildId.id.toString()) + ..commands(id: commandId.id.toString()), + "DELETE", + auth: true, + ); @override Future editGlobalCommand(Snowflake applicationId, Snowflake commandId, SlashCommandBuilder builder) async { - final response = await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/commands/$commandId", "PATCH", body: builder.build(), auth: true); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..commands(id: commandId.id.toString()), + "PATCH", + body: builder.build(), + auth: true, + ); - if (response is IHttpResponseSucess) { + if (response is IHttpResponseError) { return Future.error(response); } - return SlashCommand((response as IHttpResponseSucess).jsonBody as RawApiMap, _interactions); + return SlashCommand((response as IHttpResponseSuccess).jsonBody as RawApiMap, _interactions); } @override Future editGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId, SlashCommandBuilder builder) async { - final response = await _client.httpEndpoints - .sendRawRequest("/applications/$applicationId/guilds/$guildId/commands/$commandId", "GET", body: builder.build(), auth: true); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..guilds(id: guildId.id.toString()) + ..commands(id: commandId.id.toString()), + "GET", + body: builder.build(), + auth: true, + ); - if (response is IHttpResponseSucess) { + if (response is IHttpResponseError) { return Future.error(response); } - return SlashCommand((response as IHttpResponseSucess).jsonBody as RawApiMap, _interactions); + return SlashCommand((response as IHttpResponseSuccess).jsonBody as RawApiMap, _interactions); } @override Future fetchGlobalCommand(Snowflake applicationId, Snowflake commandId) async { - final response = await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/commands/$commandId", "GET", auth: true); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..commands(id: commandId.id.toString()), + "GET", + auth: true, + ); - if (response is IHttpResponseSucess) { + if (response is IHttpResponseError) { return Future.error(response); } - return SlashCommand((response as IHttpResponseSucess).jsonBody as RawApiMap, _interactions); + return SlashCommand((response as IHttpResponseSuccess).jsonBody as RawApiMap, _interactions); } @override - Stream fetchGlobalCommands(Snowflake applicationId) async* { - final response = await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/commands", "GET", auth: true); + Stream fetchGlobalCommands(Snowflake applicationId, {bool withLocales = true}) async* { + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..commands(), + "GET", + auth: true, + queryParams: withLocales ? {'with_localizations': withLocales.toString()} : {}, + ); if (response is IHttpResponseError) { yield* Stream.error(response); } - for (final commandSlash in (response as IHttpResponseSucess).jsonBody as List) { + for (final commandSlash in (response as IHttpResponseSuccess).jsonBody as List) { yield SlashCommand(commandSlash as RawApiMap, _interactions); } } @override Future fetchGuildCommand(Snowflake applicationId, Snowflake commandId, Snowflake guildId) async { - final response = await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/guilds/$guildId/commands/$commandId", "GET", auth: true); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..guilds(id: guildId.id.toString()) + ..commands(id: commandId.id.toString()), + "GET", + auth: true, + ); - if (response is IHttpResponseSucess) { + if (response is IHttpResponseError) { return Future.error(response); } - return SlashCommand((response as IHttpResponseSucess).jsonBody as RawApiMap, _interactions); + return SlashCommand((response as IHttpResponseSuccess).jsonBody as RawApiMap, _interactions); } @override - Stream fetchGuildCommands(Snowflake applicationId, Snowflake guildId) async* { - final response = await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/guilds/$guildId/commands", "GET", auth: true); + Stream fetchGuildCommands(Snowflake applicationId, Snowflake guildId, {bool withLocales = true}) async* { + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..guilds(id: guildId.id.toString()) + ..commands(), + "GET", + auth: true, + queryParams: withLocales ? {'with_localizations': withLocales.toString()} : {}, + ); if (response is IHttpResponseError) { yield* Stream.error(response); } - for (final commandSlash in (response as IHttpResponseSucess).jsonBody as List) { + for (final commandSlash in (response as IHttpResponseSuccess).jsonBody as List) { yield SlashCommand(commandSlash as RawApiMap, _interactions); } } @Deprecated("This endpoint requires OAuth2 authentication, which nyxx_interactions doesn't support." - " Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiresPermissions instead.") + " Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiredPermissions instead.") Future bulkOverrideGlobalCommandsPermissions(Snowflake applicationId, Iterable builders) async { final globalBody = builders .where((builder) => builder.permissions != null && builder.permissions!.isNotEmpty) @@ -331,12 +448,20 @@ class InteractionsEndpoints implements IInteractionsEndpoints { }) .toList(); - await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/commands/permissions", "PUT", body: globalBody, auth: true); + await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..commands() + ..permissions(), + "PUT", + body: globalBody, + auth: true, + ); } @override @Deprecated("This endpoint requires OAuth2 authentication, which nyxx_interactions doesn't support." - " Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiresPermissions instead.") + " Use SlashCommandBuilder.canBeUsedInDm and SlashCommandBuilder.requiredPermissions instead.") Future bulkOverrideGuildCommandsPermissions(Snowflake applicationId, Snowflake guildId, Iterable builders) async { final guildBody = builders .where((b) => b.permissions != null && b.permissions!.isNotEmpty) @@ -346,28 +471,49 @@ class InteractionsEndpoints implements IInteractionsEndpoints { }) .toList(); - await _client.httpEndpoints.sendRawRequest("/applications/$applicationId/guilds/$guildId/commands/permissions", "PUT", body: guildBody, auth: true); + await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: applicationId.id.toString()) + ..guilds(id: guildId.id.toString()) + ..commands() + ..permissions(), + "PUT", + body: guildBody, + auth: true, + ); } @override Future fetchFollowup(String token, Snowflake applicationId, Snowflake messageId) async { - final result = await _client.httpEndpoints.sendRawRequest("/webhooks/$applicationId/$token/messages/${messageId.toString()}", "GET", auth: true); + final result = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..webhooks(id: applicationId.id.toString(), token: token) + ..messages(id: messageId.id.toString()), + "GET", + auth: true, + ); if (result is IHttpResponseError) { return Future.error(result); } - return Message(_client, (result as IHttpResponseSucess).jsonBody as RawApiMap); + return Message(_client, (result as IHttpResponseSuccess).jsonBody as RawApiMap); } @override Future respondToAutocomplete(Snowflake interactionId, String token, List builders) async { - final result = await _client.httpEndpoints.sendRawRequest("/interactions/${interactionId.toString()}/$token/callback", "POST", body: { - "type": 8, - "data": { - "choices": [for (final builder in builders) builder.build()] - } - }); + final result = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..interactions(id: interactionId.id.toString(), token: token) + ..callback(), + "POST", + body: { + "type": 8, + "data": { + "choices": [for (final builder in builders) builder.build()] + } + }, + ); if (result is IHttpResponseError) { return Future.error(result); @@ -376,10 +522,16 @@ class InteractionsEndpoints implements IInteractionsEndpoints { @override Future respondModal(String token, String interactionId, ModalBuilder builder) async { - final response = await _client.httpEndpoints.sendRawRequest("/interactions/${interactionId.toString()}/$token/callback", "POST", body: { - "type": 9, - "data": {...builder.build()}, - }); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..interactions(id: interactionId, token: token) + ..callback(), + "POST", + body: { + "type": 9, + "data": {...builder.build()}, + }, + ); if (response is IHttpResponseError) { return Future.error(response); @@ -389,21 +541,28 @@ class InteractionsEndpoints implements IInteractionsEndpoints { @override Future fetchCommandOverrides(Snowflake commandId, Snowflake guildId) async { try { - final response = - await _client.httpEndpoints.sendRawRequest("/applications/${_client.appId}/guilds/$guildId/commands/$commandId/permissions", "GET", auth: true); - - return SlashCommandPermissionOverrides((response as IHttpResponseSucess).jsonBody as Map, _client); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: _client.appId.id.toString()) + ..guilds(id: guildId.id.toString()) + ..commands(id: commandId.id.toString()) + ..permissions(), + "GET", + auth: true, + ); + + return SlashCommandPermissionOverrides((response as IHttpResponseSuccess).jsonBody as Map, _client); } on IHttpResponseError catch (response) { try { // 10066 = Unknown application command permissions // Means there are no overrides for this command... why is this an error, Discord? - if (jsonDecode(response.errorMessage)['code'] == 10066) { + if (jsonDecode(response.message)['code'] == 10066) { _logger.finest('Got error code 10066 on permissions for command $commandId in guild $guildId, returning empty permission overrides.'); return SlashCommandPermissionOverrides.empty(commandId, _client); } } on Exception { // We got invalid JSON. The request is probably invalid, so we ignore it and return an error. - _logger.warning('Invalid JSON in response: ${response.errorMessage}'); + _logger.warning('Invalid JSON in response: ${response.message}'); } return Future.error(response); @@ -412,14 +571,22 @@ class InteractionsEndpoints implements IInteractionsEndpoints { @override Future> fetchPermissionOverrides(Snowflake guildId) async { - final response = await _client.httpEndpoints.sendRawRequest("/applications/${_client.appId}/guilds/$guildId/commands/permissions", "GET", auth: true); + final response = await _client.httpEndpoints.sendRawRequest( + IHttpRoute() + ..applications(id: _client.appId.id.toString()) + ..guilds(id: guildId.id.toString()) + ..commands() + ..permissions(), + "GET", + auth: true, + ); if (response is IHttpResponseError) { return Future.error(response); } List overrides = - ((response as IHttpResponseSucess).jsonBody as List).cast().map((d) => SlashCommandPermissionOverrides(d, _client)).toList(); + ((response as IHttpResponseSuccess).jsonBody as List).cast().map((d) => SlashCommandPermissionOverrides(d, _client)).toList(); for (final override in overrides) { _interactions.permissionOverridesCache[guildId] ??= {}; diff --git a/lib/src/internal/utils.dart b/lib/src/internal/utils.dart index 2249179..f270eab 100644 --- a/lib/src/internal/utils.dart +++ b/lib/src/internal/utils.dart @@ -26,7 +26,10 @@ Iterable> partition(Iterable list, bool Function(T) predicate) /// Determine what handler should be executed based on [interaction] String determineInteractionCommandHandler(ISlashCommandInteraction interaction) { - final commandHash = interaction.name; + String commandHash = interaction.name; + if (interaction.guild != null) { + commandHash = '${interaction.guild!.id}/$commandHash'; + } try { final subCommandGroup = interaction.options.firstWhere((element) => element.type == CommandOptionType.subCommandGroup); diff --git a/lib/src/models/command_option.dart b/lib/src/models/command_option.dart index 392e08b..24f4f44 100644 --- a/lib/src/models/command_option.dart +++ b/lib/src/models/command_option.dart @@ -1,6 +1,7 @@ import 'package:nyxx/nyxx.dart'; import 'package:nyxx_interactions/src/models/arg_choice.dart'; +import 'package:nyxx_interactions/src/models/locale.dart'; /// The type that a user should input for a [CommandOptionBuilder] class CommandOptionType extends IEnum { @@ -69,6 +70,12 @@ abstract class ICommandOption { /// If the option is a subcommand or subcommand group type, this nested options will be the parameters List get options; + + /// The localizations for the name of the option. + Map? get localizationsName; + + /// The localizations for the description of the option. + Map? get localizationsDescription; } /// An argument for a [SlashCommand]. @@ -107,12 +114,20 @@ class CommandOption implements ICommandOption { @override late final List options; - /// Creates na instance of [CommandOption] + @override + late final Map? localizationsName; + + @override + late final Map? localizationsDescription; + + /// Creates an instance of [CommandOption] CommandOption(RawApiMap raw) { type = CommandOptionType(raw["type"] as int); name = raw["name"] as String; description = raw["description"] as String; required = raw["required"] as bool? ?? false; + localizationsName = (raw['name_localizations'] as RawApiMap?)?.map((key, value) => MapEntry(Locale.deserialize(key), value.toString())); + localizationsDescription = (raw['description_localizations'] as RawApiMap?)?.map((key, value) => MapEntry(Locale.deserialize(key), value.toString())); choices = [ if (raw["choices"] != null) diff --git a/lib/src/models/locale.dart b/lib/src/models/locale.dart new file mode 100644 index 0000000..23435c8 --- /dev/null +++ b/lib/src/models/locale.dart @@ -0,0 +1,45 @@ +enum Locale { + danish('da'), + german('de'), + englishUk('en-GB'), + englishUs('en-US'), + spanish('es-ES'), + french('fr'), + croatian('hr'), + italian('it'), + lithuanian('lt'), + hungarian('hu'), + dutch('nl'), + norwegian('no'), + polish('pl'), + portugueseBrazilian('pt-BR'), + romanian('ro'), + finnish('fi'), + swedish('sv-SE'), + vietnamese('vi'), + turkish('tr'), + czech('cs'), + greek('el'), + bulgarian('bg'), + russian('ru'), + ukrainian('uk'), + hindi('hi'), + thai('th'), + chineseChina('zh-CN'), + japanese('ja'), + chineseTaiwan('zh-TW'), + korean('ko'); + + final String code; + + const Locale(this.code); + + /// Deserializes the [code] into a [Locale]. If the [code] is not a valid locale, + /// returns the [Locale.englishUs] as it is considered the default locale from Discord. + static Locale deserialize(String code) { + return values.firstWhere((e) => e.code == code, orElse: () => Locale.englishUs); + } + + @override + String toString() => code; +} diff --git a/lib/src/models/slash_command.dart b/lib/src/models/slash_command.dart index f878ef1..728a458 100644 --- a/lib/src/models/slash_command.dart +++ b/lib/src/models/slash_command.dart @@ -26,7 +26,7 @@ abstract class ISlashCommand implements SnowflakeEntity { Cacheable? get guild; /// Whether the command is enabled by default when the app is added to a guild - @Deprecated('Use canBeUsedInDm and requiresPermissions instead') + @Deprecated('Use canBeUsedInDm and requiredPermissions instead') bool get defaultPermissions; /// Whether this slash command can be used in a DM channel with the bot. @@ -41,6 +41,12 @@ abstract class ISlashCommand implements SnowflakeEntity { /// If this command is a guild command, the permission overrides attached to this command, `null` otherwise. Cacheable? get permissionOverrides; + /// The localized names of the command. + Map? get localizationsName; + + /// The localized descriptions of the command. + Map? get localizationsDescription; + /// Get the permission overrides for this command in a specific guild. Cacheable getPermissionOverridesInGuild(Snowflake guildId); } @@ -73,7 +79,7 @@ class SlashCommand extends SnowflakeEntity implements ISlashCommand { /// Whether the command is enabled by default when the app is added to a guild @override - @Deprecated('Use canBeUsedInDm and requiresPermissions instead') + @Deprecated('Use canBeUsedInDm and requiredPermissions instead') late final bool defaultPermissions; /// Whether this slash command can be used in a DM channel with the bot. @@ -87,6 +93,12 @@ class SlashCommand extends SnowflakeEntity implements ISlashCommand { @override late final int requiredPermissions; + @override + late final Map? localizationsName; + + @override + late final Map? localizationsDescription; + @override late final Cacheable? permissionOverrides; @@ -101,6 +113,8 @@ class SlashCommand extends SnowflakeEntity implements ISlashCommand { guild = raw["guild_id"] != null ? GuildCacheable(_interactions.client, Snowflake(raw["guild_id"])) : null; canBeUsedInDm = raw["dm_permission"] as bool? ?? true; requiredPermissions = int.parse(raw["default_member_permissions"] as String? ?? "0"); + localizationsName = (raw['name_localizations'] as RawApiMap?)?.map((key, value) => MapEntry(Locale.deserialize(key), value.toString())); + localizationsDescription = (raw['description_localizations'] as RawApiMap?)?.map((key, value) => MapEntry(Locale.deserialize(key), value.toString())); if (guild != null) { permissionOverrides = SlashCommandPermissionOverridesCacheable(id, guild!.id, _interactions); diff --git a/lib/src/models/slash_command_permission.dart b/lib/src/models/slash_command_permission.dart index e022b61..55ee06a 100644 --- a/lib/src/models/slash_command_permission.dart +++ b/lib/src/models/slash_command_permission.dart @@ -60,7 +60,7 @@ abstract class ISlashCommandPermissionOverrides implements SnowflakeEntity { /// The permissions attached to the command. List get permissionOverrides; - /// Whether these overrideas are global across all commands in a guild. + /// Whether these overrides are global across all commands in a guild. bool get isGlobal; } diff --git a/pubspec.yaml b/pubspec.yaml index 4137f06..68f40ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,18 +1,18 @@ name: nyxx_interactions -version: 4.2.1 +version: 4.3.0 description: Nyxx Interactions Module. Discord library for Dart. Simple, robust framework for creating discord bots for Dart language. -homepage: https://github.com/nyxx-discord/nyxx -repository: https://github.com/nyxx-discord/nyxx +homepage: https://github.com/nyxx-discord/nyxx_interactions +repository: https://github.com/nyxx-discord/nyxx_interactions documentation: https://nyxx.l7ssha.xyz -issue_tracker: https://github.com/nyxx-discord/nyxx/issues +issue_tracker: https://github.com/nyxx-discord/nyxx_interactions/issues environment: - sdk: '>=2.14.0 <3.0.0' + sdk: '>=2.17.0 <3.0.0' dependencies: crypto: ^3.0.1 logging: ^1.0.1 - nyxx: ^3.3.0 + nyxx: ^4.0.0 dev_dependencies: test: ^1.19.0 diff --git a/test/unit/command_option_builder.dart b/test/unit/command_option_builder.dart index 2e5b89b..9d873e9 100755 --- a/test/unit/command_option_builder.dart +++ b/test/unit/command_option_builder.dart @@ -24,7 +24,15 @@ main() { channelTypes: [ ChannelType.text, ], - autoComplete: true); + autoComplete: true, + localizationsName: { + Locale.german: 'testen', + Locale.french: 'tester', + }, + localizationsDescription: { + Locale.german: 'testen', + Locale.french: 'tester', + }); final expectedResult = { "type": CommandOptionType.channel.value, @@ -36,7 +44,15 @@ main() { {'name': 'arg1', 'value': 'val1'} ], 'channel_types': [ChannelType.text], - 'autocomplete': true + 'autocomplete': true, + 'name_localizations': { + 'de': 'testen', + 'fr': 'tester', + }, + 'description_localizations': { + 'de': 'testen', + 'fr': 'tester', + }, }; expect(builder.build(), equals(expectedResult)); diff --git a/test/unit/slash_command_builder.dart b/test/unit/slash_command_builder.dart index d0026f2..c9d7d21 100755 --- a/test/unit/slash_command_builder.dart +++ b/test/unit/slash_command_builder.dart @@ -39,7 +39,21 @@ void main() { }); test('.build', () { - final slashCommandBuilder = SlashCommandBuilder("invalid-name", "test", [], requiredPermissions: PermissionsConstants.administrator); + final slashCommandBuilder = SlashCommandBuilder( + "invalid-name", + "test", + [], + requiredPermissions: PermissionsConstants.administrator, + localizationsName: { + Locale.french: 'nom-invalide', + Locale.german: 'unguelitger-name', // ΓΌ -> ue + }, + localizationsDescription: { + // Not litteral translations, just here to test if it works + Locale.french: 'tester', + Locale.german: 'testen', + }, + ); final expectedResult = { "name": "invalid-name", @@ -48,6 +62,14 @@ void main() { "default_permission": true, // TODO: remove when default_permission is removed "dm_permission": true, "default_member_permissions": PermissionsConstants.administrator.toString(), + "name_localizations": { + 'fr': 'nom-invalide', + 'de': 'unguelitger-name', + }, + "description_localizations": { + 'fr': 'tester', + 'de': 'testen', + }, }; expect(slashCommandBuilder.build(), equals(expectedResult));