From 9305c2a68c10aae6f2347e0b9b59b08a62564647 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Wed, 28 Feb 2024 15:58:35 +0000 Subject: [PATCH] Reformat project in line with editorconfig --- .github/disable-parallel.main.kts | 2 +- .github/get-version.main.kts | 4 +- .github/release.main.kts | 70 +- .github/tag.main.kts | 50 +- .vscode/extensions.json | 1 - annotation-processor/build.gradle.kts | 26 +- .../annotations/ConverterProcessorProvider.kt | 18 +- .../annotations/PluginProcessorProvider.kt | 10 +- .../extensions/modules/annotations/_Utils.kt | 72 +- .../converters/ConverterAnnotationArgs.kt | 126 +- .../converters/ConverterFunctionBuilder.kt | 384 ++--- .../converters/ConverterProcessor.kt | 442 +++--- .../annotations/converters/Functions.kt | 782 ++++----- .../builders/ConverterBuilderClassBuilder.kt | 600 +++---- .../ConverterBuilderFunctionBuilder.kt | 162 +- .../modules/annotations/plugins/PluginArgs.kt | 68 +- .../annotations/plugins/PluginProcessor.kt | 194 +-- annotations/build.gradle.kts | 16 +- .../annotations/converters/Converter.kt | 28 +- .../annotations/converters/ConverterType.kt | 44 +- .../annotations/plugins/WiredPlugin.kt | 14 +- .../extensions/tooling/TranslatableType.kt | 2 +- build.gradle.kts | 72 +- buildSrc/build.gradle.kts | 24 +- buildSrc/src/main/kotlin/GitTools.kt | 26 +- buildSrc/src/main/kotlin/ProjectTools.kt | 8 +- .../disable-explicit-api-mode.gradle.kts | 24 +- .../src/main/kotlin/kordex-module.gradle.kts | 2 - .../src/main/kotlin/ksp-module.gradle.kts | 42 +- .../main/kotlin/published-module.gradle.kts | 144 +- .../src/main/kotlin/tested-module.gradle.kts | 18 +- data-adapters/adapter-mongodb/README.md | 6 +- .../adapter-mongodb/build.gradle.kts | 32 +- detekt.yml | 134 +- extra-modules/extra-mappings/README.md | 13 +- extra-modules/extra-mappings/build.gradle.kts | 84 +- .../modules/extra/mappings/Checks.kt | 252 +-- .../modules/extra/mappings/Functions.kt | 8 +- .../extra/mappings/arguments/BarnArguments.kt | 10 +- .../mappings/arguments/FeatherArguments.kt | 10 +- .../arguments/HashedMojangArguments.kt | 20 +- .../arguments/IntermediaryMappable.kt | 8 +- .../mappings/arguments/LegacyYarnArguments.kt | 10 +- .../mappings/arguments/MappingArguments.kt | 22 +- .../arguments/MappingConversionArguments.kt | 66 +- .../arguments/MappingWithChannelArguments.kt | 2 +- .../mappings/arguments/MojangArguments.kt | 20 +- .../mappings/arguments/PlasmaArguments.kt | 10 +- .../mappings/arguments/QuiltArguments.kt | 10 +- .../mappings/arguments/SrgMojangArguments.kt | 10 +- .../extra/mappings/arguments/YarnArguments.kt | 20 +- .../mappings/arguments/YarrnArguments.kt | 10 +- .../mappings/builders/ExtMappingsBuilder.kt | 24 +- .../converters/MappingsVersionConverter.kt | 78 +- .../modules/extra/mappings/enums/Channels.kt | 4 +- .../UnsupportedNamespaceException.kt | 2 +- .../modules/extra/mappings/utils/Arguments.kt | 32 +- .../modules/extra/mappings/utils/Mappings.kt | 996 ++++++------ .../mappings/utils/McpNamespaceReplacement.kt | 310 ++-- .../mappings/utils/MojangReleaseContainer.kt | 48 +- .../mappings/utils/YarnReleaseContainer.kt | 22 +- .../extra/mappings/utils/linkie/Functions.kt | 16 +- .../plugins/extra/MappingsPlugin.kt | 26 +- .../translations/kordex/mappings.properties | 7 +- .../kordex/mappings_en_GB.properties | 7 +- .../kord/extensions/test/bot/Bot.kt | 30 +- .../test/resources/junit-platform.properties | 1 - .../src/test/resources/logback.groovy | 26 +- extra-modules/extra-phishing/README.md | 27 +- extra-modules/extra-phishing/build.gradle.kts | 34 +- .../modules/extra/phishing/DetectionAction.kt | 16 +- .../modules/extra/phishing/DomainChange.kt | 12 +- .../extra/phishing/ExtPhishingBuilder.kt | 142 +- .../modules/extra/phishing/Functions.kt | 8 +- .../modules/extra/phishing/PhishingApi.kt | 50 +- .../extra/phishing/PhishingExtension.kt | 722 ++++----- extra-modules/extra-pluralkit/README.md | 9 +- .../extra-pluralkit/build.gradle.kts | 32 +- .../modules/extra/pluralkit/PKExtension.kt | 984 ++++++------ .../modules/extra/pluralkit/_Utils.kt | 2 +- .../modules/extra/pluralkit/api/PKMember.kt | 38 +- .../extra/pluralkit/api/PKMemberPrivacy.kt | 28 +- .../modules/extra/pluralkit/api/PKMessage.kt | 16 +- .../modules/extra/pluralkit/api/PKProxyTag.kt | 6 +- .../modules/extra/pluralkit/api/PKSystem.kt | 26 +- .../extra/pluralkit/api/PKSystemPrivacy.kt | 28 +- .../modules/extra/pluralkit/api/PluralKit.kt | 146 +- .../pluralkit/events/PKMessageCreateEvent.kt | 214 +-- .../pluralkit/events/PKMessageDeleteEvent.kt | 234 +-- .../pluralkit/events/PKMessageUpdateEvent.kt | 250 +-- .../extra/pluralkit/storage/PKGuildConfig.kt | 32 +- .../extra/pluralkit/utils/LRUHashMap.kt | 4 +- .../translations/kordex/pluralkit.properties | 9 - .../kordex/pluralkit_en_GB.properties | 1 - .../kordex/pluralkit_pt.properties | 2 - extra-modules/extra-tags/build.gradle.kts | 34 +- extra-modules/extra-welcome/build.gradle.kts | 34 +- gradle.properties | 11 +- gradle/libs.versions.toml | 6 +- kord-extensions/build.gradle.kts | 76 +- .../kord/extensions/ExtensibleBot.kt | 868 +++++----- .../kord/extensions/annotations/DoNotChain.kt | 8 +- .../kord/extensions/commands/Argument.kt | 14 +- .../kord/extensions/commands/Arguments.kt | 352 ++--- .../kord/extensions/commands/Command.kt | 182 +-- .../extensions/commands/CommandContext.kt | 110 +- .../application/ApplicationCommand.kt | 336 ++-- .../application/ApplicationCommandContext.kt | 78 +- .../application/ApplicationCommandRegistry.kt | 1008 ++++++------ .../DefaultApplicationCommandRegistry.kt | 680 ++++---- .../DummyAutocompleteCommandContext.kt | 28 +- .../StorageAwareApplicationCommandRegistry.kt | 238 +-- .../message/EphemeralMessageCommand.kt | 176 +-- .../message/EphemeralMessageCommandContext.kt | 8 +- .../message/MessageCommandContext.kt | 10 +- .../message/PublicMessageCommand.kt | 176 +-- .../message/PublicMessageCommandContext.kt | 8 +- .../slash/EphemeralSlashCommand.kt | 292 ++-- .../slash/EphemeralSlashCommandContext.kt | 8 +- .../application/slash/PublicSlashCommand.kt | 246 +-- .../slash/PublicSlashCommandContext.kt | 8 +- .../application/slash/SlashCommand.kt | 452 +++--- .../application/slash/SlashCommandContext.kt | 18 +- .../application/slash/SlashCommandParser.kt | 580 +++---- .../commands/application/slash/SlashGroup.kt | 90 +- .../commands/application/slash/_Functions.kt | 448 +++--- .../slash/converters/ChoiceConverter.kt | 12 +- .../slash/converters/ChoiceEnum.kt | 4 +- .../converters/impl/EnumChoiceConverter.kt | 130 +- .../converters/impl/NumberChoiceConverter.kt | 62 +- .../converters/impl/StringChoiceConverter.kt | 40 +- .../application/user/EphemeralUserCommand.kt | 222 +-- .../user/EphemeralUserCommandContext.kt | 8 +- .../application/user/PublicUserCommand.kt | 222 +-- .../user/PublicUserCommandContext.kt | 8 +- .../commands/application/user/UserCommand.kt | 154 +- .../application/user/UserCommandContext.kt | 10 +- .../extensions/commands/chat/ChatCommand.kt | 852 +++++----- .../commands/chat/ChatCommandContext.kt | 205 +-- .../commands/chat/ChatCommandParser.kt | 1402 ++++++++--------- .../commands/chat/ChatGroupCommand.kt | 414 ++--- .../commands/chat/ChatSubCommand.kt | 46 +- .../commands/converters/Annotations.kt | 30 +- .../converters/CoalescingConverter.kt | 184 +-- .../CoalescingToDefaultingConverter.kt | 66 +- .../CoalescingToOptionalConverter.kt | 64 +- .../commands/converters/Converter.kt | 216 +-- .../DefaultingCoalescingConverter.kt | 38 +- .../converters/DefaultingConverter.kt | 38 +- .../commands/converters/ListConverter.kt | 36 +- .../converters/OptionalCoalescingConverter.kt | 36 +- .../commands/converters/OptionalConverter.kt | 36 +- .../commands/converters/SingleConverter.kt | 250 +-- .../converters/SingleToDefaultingConverter.kt | 76 +- .../converters/SingleToListConverter.kt | 92 +- .../converters/SingleToOptionalConverter.kt | 2 +- .../converters/SlashCommandConverter.kt | 16 +- .../extensions/commands/converters/_Types.kt | 2 +- .../builders/ChoiceConverterBuilder.kt | 20 +- .../converters/builders/ConverterBuilder.kt | 120 +- .../DefaultingCoalescingConverterBuilder.kt | 14 +- .../builders/DefaultingConverterBuilder.kt | 20 +- .../builders/ListConverterBuilder.kt | 4 +- .../builders/OptionalConverterBuilder.kt | 4 +- .../converters/builders/ValidationContext.kt | 358 ++--- .../converters/impl/AttachmentConverter.kt | 28 +- .../converters/impl/BooleanConverter.kt | 38 +- .../converters/impl/DecimalConverter.kt | 98 +- .../impl/DurationCoalescingConverter.kt | 340 ++-- .../converters/impl/DurationConverter.kt | 164 +- .../converters/impl/EmailConverter.kt | 54 +- .../commands/converters/impl/EnumConverter.kt | 86 +- .../converters/impl/GuildConverter.kt | 74 +- .../commands/converters/impl/IntConverter.kt | 128 +- .../commands/converters/impl/LongConverter.kt | 128 +- .../converters/impl/MemberConverter.kt | 196 +-- .../converters/impl/MessageConverter.kt | 316 ++-- .../impl/RegexCoalescingConverter.kt | 44 +- .../converters/impl/RegexConverter.kt | 40 +- .../commands/converters/impl/RoleConverter.kt | 136 +- .../converters/impl/SnowflakeConverter.kt | 32 +- .../impl/StringCoalescingConverter.kt | 36 +- .../converters/impl/StringConverter.kt | 88 +- .../impl/SupportedLocaleConverter.kt | 42 +- .../commands/converters/impl/TagConverter.kt | 262 +-- .../converters/impl/TimestampConverter.kt | 92 +- .../commands/converters/impl/UserConverter.kt | 156 +- .../commands/events/ChatCommandEvents.kt | 26 +- .../commands/events/CommandEvents.kt | 20 +- .../commands/events/MessageCommandEvents.kt | 52 +- .../commands/events/SlashCommandEvents.kt | 64 +- .../commands/events/UserCommandEvents.kt | 50 +- .../kord/extensions/components/Component.kt | 40 +- .../extensions/components/ComponentContext.kt | 256 +-- .../components/ComponentRegistry.kt | 124 +- .../components/ComponentWithAction.kt | 268 ++-- .../extensions/components/ComponentWithID.kt | 14 +- .../buttons/DisabledInteractionButton.kt | 30 +- .../buttons/EphemeralInteractionButton.kt | 230 +-- .../EphemeralInteractionButtonContext.kt | 8 +- .../components/buttons/InteractionButton.kt | 16 +- .../buttons/InteractionButtonContext.kt | 6 +- .../buttons/InteractionButtonWithAction.kt | 102 +- .../buttons/InteractionButtonWithID.kt | 18 +- .../buttons/LinkInteractionButton.kt | 28 +- .../buttons/PublicInteractionButton.kt | 230 +-- .../buttons/PublicInteractionButtonContext.kt | 8 +- .../kord/extensions/components/forms/Form.kt | 8 +- .../extensions/components/forms/ModalForm.kt | 8 +- .../kord/extensions/components/forms/_Grid.kt | 280 ++-- .../forms/widgets/LineTextWidget.kt | 2 +- .../forms/widgets/ParagraphTextWidget.kt | 2 +- .../forms/widgets/TextInputWidget.kt | 178 +-- .../components/forms/widgets/Widget.kt | 30 +- .../components/menus/EphemeralSelectMenu.kt | 212 +-- .../components/menus/PublicSelectMenu.kt | 212 +-- .../components/menus/SelectMenuContext.kt | 6 +- .../menus/channel/ChannelSelectMenu.kt | 40 +- .../menus/channel/ChannelSelectMenuContext.kt | 16 +- .../channel/EphemeralChannelSelectMenu.kt | 24 +- .../EphemeralChannelSelectMenuContext.kt | 8 +- .../menus/channel/PublicChannelSelectMenu.kt | 24 +- .../channel/PublicChannelSelectMenuContext.kt | 8 +- .../menus/role/EphemeralRoleSelectMenu.kt | 22 +- .../role/EphemeralRoleSelectMenuContext.kt | 8 +- .../menus/role/PublicRoleSelectMenu.kt | 22 +- .../menus/role/PublicRoleSelectMenuContext.kt | 8 +- .../components/menus/role/RoleSelectMenu.kt | 18 +- .../menus/role/RoleSelectMenuContext.kt | 28 +- .../menus/string/EphemeralStringSelectMenu.kt | 24 +- .../EphemeralStringSelectMenuContext.kt | 8 +- .../menus/string/PublicStringSelectMenu.kt | 24 +- .../string/PublicStringSelectMenuContext.kt | 8 +- .../menus/string/StringSelectMenu.kt | 123 +- .../menus/string/StringSelectMenuContext.kt | 10 +- .../menus/user/EphemeralUserSelectMenu.kt | 22 +- .../user/EphemeralUserSelectMenuContext.kt | 8 +- .../menus/user/PublicUserSelectMenu.kt | 22 +- .../menus/user/PublicUserSelectMenuContext.kt | 8 +- .../components/menus/user/UserSelectMenu.kt | 18 +- .../menus/user/UserSelectMenuContext.kt | 16 +- .../components/types/HasPartialEmoji.kt | 48 +- .../kord/extensions/events/EventContext.kt | 112 +- .../kord/extensions/events/EventHandler.kt | 400 ++--- .../extensions/events/ExtensionStateEvent.kt | 4 +- .../kord/extensions/events/KordExEvent.kt | 10 +- .../events/ModalInteractionCompleteEvent.kt | 4 +- .../extra/GuildJoinRequestDeleteEvent.kt | 34 +- .../extra/GuildJoinRequestUpdateEvent.kt | 38 +- .../events/extra/models/ApplicationStatus.kt | 12 +- .../events/extra/models/GuildJoinRequest.kt | 50 +- .../extra/models/GuildJoinRequestDelete.kt | 10 +- .../extra/models/GuildJoinRequestResponse.kt | 68 +- .../extra/models/GuildJoinRequestUpdate.kt | 8 +- .../extra/models/GuildJoinRequestUser.kt | 18 +- .../events/interfaces/ChannelEvent.kt | 16 +- .../events/interfaces/GuildEvent.kt | 12 +- .../events/interfaces/MemberEvent.kt | 12 +- .../events/interfaces/MessageEvent.kt | 12 +- .../extensions/events/interfaces/RoleEvent.kt | 12 +- .../extensions/events/interfaces/UserEvent.kt | 12 +- .../kord/extensions/extensions/Extension.kt | 390 ++--- .../extensions/extensions/ExtensionState.kt | 12 +- .../kord/extensions/extensions/_Commands.kt | 474 +++--- .../kord/extensions/extensions/_Events.kt | 40 +- .../extensions/base/HelpProvider.kt | 340 ++-- .../extensions/impl/HelpExtension.kt | 806 +++++----- .../extensions/impl/SentryExtension.kt | 174 +- .../i18n/ResourceBundleTranslations.kt | 349 ++-- .../kord/extensions/i18n/SupportedLocales.kt | 230 +-- .../extensions/i18n/TranslationsProvider.kt | 278 ++-- .../extensions/koin/KordExKoinComponent.kt | 12 +- .../pagination/EphemeralResponsePaginator.kt | 68 +- .../pagination/MessageButtonPaginator.kt | 124 +- .../pagination/PublicFollowUpPaginator.kt | 86 +- .../pagination/PublicResponsePaginator.kt | 70 +- .../kord/extensions/pagination/_Functions.kt | 36 +- .../pagination/builders/PaginatorBuilder.kt | 64 +- .../kord/extensions/pagination/pages/Page.kt | 23 +- .../kord/extensions/pagination/pages/Pages.kt | 60 +- .../kord/extensions/parsers/BooleanParser.kt | 60 +- .../kord/extensions/parsers/ColorParser.kt | 20 +- .../kord/extensions/parsers/DurationParser.kt | 122 +- .../kord/extensions/parsers/Exceptions.kt | 4 +- .../extensions/parsers/caches/ColorCache.kt | 46 +- .../parsers/caches/TimeUnitCache.kt | 62 +- .../kord/extensions/plugins/DefaultLoader.kt | 20 +- .../extensions/plugins/DevelopmentLoader.kt | 20 +- .../kord/extensions/plugins/JarLoader.kt | 20 +- .../kord/extensions/plugins/KordExPlugin.kt | 136 +- .../kord/extensions/plugins/PluginManager.kt | 16 +- ...uctingApplicationCommandRegistryStorage.kt | 108 +- .../registry/DefaultLocalRegistryStorage.kt | 30 +- .../extensions/registry/RegistryStorage.kt | 92 +- .../kord/extensions/sentry/BreadcrumbType.kt | 40 +- .../extensions/sentry/SentryIdConverter.kt | 52 +- .../sentry/captures/SentryExceptionCapture.kt | 2 +- .../kord/extensions/storage/DataAdapter.kt | 154 +- .../storage/StorageTypeSerializer.kt | 20 +- .../kord/extensions/storage/StorageUnit.kt | 494 +++--- .../kord/extensions/storage/_Utils.kt | 2 +- .../storage/toml/TomlDataAdapter.kt | 125 +- .../kord/extensions/time/TimestampType.kt | 90 +- .../kord/extensions/time/Utils.kt | 57 +- .../kord/extensions/types/FailureReason.kt | 36 +- .../kord/extensions/types/Lockable.kt | 60 +- .../extensions/types/TranslatableContext.kt | 92 +- .../kord/extensions/utils/Mentions.kt | 12 +- .../kord/extensions/utils/_Attachments.kt | 58 +- .../kord/extensions/utils/_Channel.kt | 42 +- .../kord/extensions/utils/_ChannelType.kt | 30 +- .../kord/extensions/utils/_Components.kt | 48 +- .../kord/extensions/utils/_Durations.kt | 26 +- .../kord/extensions/utils/_Environment.kt | 82 +- .../kord/extensions/utils/_Guilds.kt | 14 +- .../kord/extensions/utils/_Interactions.kt | 276 ++-- .../kord/extensions/utils/_Koin.kt | 10 +- .../kord/extensions/utils/_Kord.kt | 56 +- .../kord/extensions/utils/_Maps.kt | 35 +- .../kord/extensions/utils/_Member.kt | 104 +- .../kord/extensions/utils/_Message.kt | 423 +++-- .../kord/extensions/utils/_Misc.kt | 24 +- .../kord/extensions/utils/_NsfwLevels.kt | 64 +- .../kord/extensions/utils/_Permissions.kt | 128 +- .../kord/extensions/utils/_RestRequest.kt | 12 +- .../kord/extensions/utils/_Role.kt | 8 +- .../kord/extensions/utils/_String.kt | 28 +- .../kord/extensions/utils/_Translation.kt | 72 +- .../kord/extensions/utils/_Users.kt | 44 +- .../kord/extensions/utils/deltas/ChangeSet.kt | 272 ++-- .../extensions/utils/deltas/MemberDelta.kt | 96 +- .../extensions/utils/deltas/MessageDelta.kt | 144 +- .../kord/extensions/utils/deltas/RoleDelta.kt | 58 +- .../kord/extensions/utils/deltas/UserDelta.kt | 70 +- .../extensions/utils/scheduling/Scheduler.kt | 154 +- .../kord/extensions/utils/scheduling/Task.kt | 248 +-- .../translations/kordex/strings.properties | 3 - .../translations/kordex/strings_ar.properties | 1 - .../kordex/strings_de_DE.properties | 1 - .../kordex/strings_en_GB.properties | 1 - .../translations/kordex/strings_es.properties | 1 - .../kordex/strings_fi_FI.properties | 1 - .../kordex/strings_fr_FR.properties | 1 - .../translations/kordex/strings_it.properties | 1 - .../translations/kordex/strings_ja.properties | 1 - .../translations/kordex/strings_ko.properties | 2 - .../kordex/strings_pl_PL.properties | 1 - .../kordex/strings_pt_PT.properties | 1 - .../kordex/strings_ru_RU.properties | 1 - .../kordex/strings_tok.properties | 2 - .../translations/kordex/strings_tr.properties | 1 - .../kordex/strings_zh_CN.properties | 1 - .../converters/impl/TimestampConverterTest.kt | 52 +- .../kord/extensions/test/SchedulerTest.kt | 134 +- .../kord/extensions/utils/MapTest.kt | 56 +- .../kord/extensions/utils/StringTest.kt | 82 +- .../test/resources/junit-platform.properties | 1 - .../src/test/resources/logback.groovy | 24 +- .../translations/test/strings.properties | 1 - .../test/strings_en_GB.properties | 1 - .../test/strings_en_US.properties | 1 - .../translations/test/strings_ja.properties | 1 - modules/java-time/build.gradle.kts | 36 +- .../modules/time/java/ChronoContainer.kt | 524 +++--- .../time/java/ChronoContainerSerializer.kt | 36 +- .../java/J8DurationCoalescingConverter.kt | 292 ++-- .../modules/time/java/J8DurationConverter.kt | 122 +- .../modules/time/java/J8DurationParser.kt | 80 +- .../modules/time/java/J8TimeUnitCache.kt | 46 +- .../extensions/modules/time/java/Utils.kt | 56 +- .../kord/extensions/test/bot/Bot.kt | 72 +- .../kord/extensions/test/bot/TestExtension.kt | 36 +- .../src/test/resources/logback.groovy | 26 +- modules/time4j/build.gradle.kts | 38 +- .../time4j/T4JDurationCoalescingConverter.kt | 288 ++-- .../time/time4j/T4JDurationConverter.kt | 84 +- .../modules/time/time4j/T4JDurationParser.kt | 90 +- .../modules/time/time4j/T4JTimeUnitCache.kt | 46 +- .../extensions/modules/time/time4j/Utils.kt | 88 +- .../kord/extensions/test/bot/Bot.kt | 42 +- .../kord/extensions/test/bot/TestExtension.kt | 36 +- .../time4j/src/test/resources/logback.groovy | 26 +- modules/unsafe/build.gradle.kts | 28 +- .../modules/unsafe/annotations/UnsafeAPI.kt | 4 +- .../unsafe/commands/UnsafeCommandEvents.kt | 66 +- .../unsafe/commands/UnsafeMessageCommand.kt | 2 +- .../unsafe/commands/UnsafeSlashCommand.kt | 2 +- .../unsafe/commands/UnsafeUserCommand.kt | 2 +- .../contexts/UnsafeMessageCommandContext.kt | 8 +- .../contexts/UnsafeSlashCommandContext.kt | 8 +- .../contexts/UnsafeUserCommandContext.kt | 8 +- .../unsafe/converters/UnionConverter.kt | 556 +++---- .../modules/unsafe/extensions/_Commands.kt | 124 +- .../unsafe/extensions/_SlashCommands.kt | 110 +- .../types/InitialMessageCommandResponse.kt | 40 +- .../types/InitialSlashCommandResponse.kt | 40 +- .../types/InitialUserCommandResponse.kt | 40 +- .../unsafe/types/UnsafeInteractionContext.kt | 136 +- .../test/resources/junit-platform.properties | 1 - .../src/test/resources/logback.groovy | 25 +- .../plugins/ParentPluginClassLoader.kt | 3 +- .../extensions/plugins/PluginClassLoader.kt | 2 +- .../plugins/constraints/ConstraintChecker.kt | 2 +- .../test/resources/junit-platform.properties | 1 - plugins/src/test/resources/logback.groovy | 25 +- test-bot/build.gradle.kts | 116 +- test-bot/detekt.yml | 82 +- .../kord/extensions/testbot/TestBot.kt | 138 +- .../extensions/ArgumentTestExtension.kt | 318 ++-- .../testbot/extensions/I18nTestExtension.kt | 390 ++--- .../testbot/extensions/ModalTestExtension.kt | 306 ++-- .../extensions/NestingTestExtension.kt | 68 +- .../testbot/extensions/PKTestExtension.kt | 100 +- .../extensions/PaginatorTestExtension.kt | 250 +-- .../extensions/SelectorTestExtension.kt | 264 ++-- .../extensions/testbot/plugin/TestPlugin.kt | 34 +- .../testbot/plugin/TestPluginData.kt | 4 +- .../testbot/plugin/TestPluginExtension.kt | 292 ++-- .../kord/extensions/testbot/utils/LogLevel.kt | 86 +- .../kord/extensions/testbot/utils/_Asserts.kt | 60 +- .../kord/extensions/testbot/utils/_Logging.kt | 103 +- test-bot/src/main/resources/logback.groovy | 24 +- .../translations/custom/strings.properties | 1 - .../translations/test/strings.properties | 2 - .../translations/test/strings_de.properties | 1 - .../test/strings_en_GB.properties | 1 - .../test/strings_en_US.properties | 1 - .../translations/test/strings_ja.properties | 1 - .../test/strings_zh_CN.properties | 1 - token-parser/build.gradle.kts | 34 +- .../kord/extensions/parser/Cursor.kt | 252 +-- .../kord/extensions/parser/StringParser.kt | 504 +++--- .../parser/tokens/NamedArgumentToken.kt | 4 +- .../parser/tokens/PositionalArgumentToken.kt | 2 +- .../kord/extensions/parser/tokens/Token.kt | 4 +- .../kord/extensions/test/StringParserTest.kt | 228 +-- .../test/resources/junit-platform.properties | 1 - .../src/test/resources/logback.groovy | 25 +- 438 files changed, 21550 insertions(+), 21615 deletions(-) diff --git a/.github/disable-parallel.main.kts b/.github/disable-parallel.main.kts index b12593de52..de85a537d0 100644 --- a/.github/disable-parallel.main.kts +++ b/.github/disable-parallel.main.kts @@ -1,5 +1,5 @@ import java.io.File -import java.util.Properties +import java.util.* val properties = Properties() val file = File("gradle.properties") diff --git a/.github/get-version.main.kts b/.github/get-version.main.kts index 7b5d0b1371..56abad3194 100644 --- a/.github/get-version.main.kts +++ b/.github/get-version.main.kts @@ -1,10 +1,10 @@ import java.io.File -import java.util.Properties +import java.util.* val properties = Properties() properties.load( - File("gradle.properties").inputStream() + File("gradle.properties").inputStream() ) print(properties["projectVersion"]) diff --git a/.github/release.main.kts b/.github/release.main.kts index 8f5ba8e977..0fc691eebb 100644 --- a/.github/release.main.kts +++ b/.github/release.main.kts @@ -15,7 +15,7 @@ var githubTag: String = System.getenv("GITHUB_REF") val repo: String = System.getenv("GITHUB_REPOSITORY") if (githubTag.contains("/")) { - githubTag = githubTag.split("/").last() + githubTag = githubTag.split("/").last() } println("Current tag: $githubTag") @@ -23,17 +23,17 @@ println("Current tag: $githubTag") val apiUrl = "https://api.github.com/repos/$repo/releases/tags/$githubTag" if (githubTag.contains("v")) { - githubTag = githubTag.split("v", limit = 2).last() + githubTag = githubTag.split("v", limit = 2).last() } val response = httpGet { url(apiUrl) } val responseCode = response.code() if (responseCode >= 400) { - println("API error: HTTP $responseCode") - println(response.body()?.string()) + println("API error: HTTP $responseCode") + println(response.body()?.string()) - exitProcess(1) + exitProcess(1) } val data = gson.create().fromJson>(response.body()!!.string(), Map::class.java) @@ -50,56 +50,56 @@ val releaseTime = data["published_at"] as String val releaseUrl = data["html_url"] as String if (releaseBody.startsWith("#")) { - val lines = releaseBody.split("\n").toMutableList() + val lines = releaseBody.split("\n").toMutableList() - lines[0] = lines[0].replaceFirst("#", "**") + "**" - releaseBody = lines.joinToString("\n") + lines[0] = lines[0].replaceFirst("#", "**") + "**" + releaseBody = lines.joinToString("\n") } if (releaseBody.contains("---")) { - releaseBody = releaseBody.split("---", limit = 2).first() + releaseBody = releaseBody.split("---", limit = 2).first() } val webhook = mapOf( - "embeds" to listOf( - mapOf( - "color" to 7506394, - "description" to releaseBody, - "timestamp" to releaseTime.replace("Z", ".000Z"), - "title" to releaseName, - "url" to releaseUrl, - - "author" to mapOf( - "icon_url" to authorAvatar, - "name" to authorName, - "url" to authorUrl, - ) - ) - ) + "embeds" to listOf( + mapOf( + "color" to 7506394, + "description" to releaseBody, + "timestamp" to releaseTime.replace("Z", ".000Z"), + "title" to releaseName, + "url" to releaseUrl, + + "author" to mapOf( + "icon_url" to authorAvatar, + "name" to authorName, + "url" to authorUrl, + ) + ) + ) ) val jsonBody = gson.create().toJson(webhook) val webhookResponse = httpPost { - url(webhookUrl) + url(webhookUrl) - header { - "User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) " + - "Chrome/35.0.1916.47 Safari/537.36" - } + header { + "User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) " + + "Chrome/35.0.1916.47 Safari/537.36" + } - body { - json(jsonBody) - } + body { + json(jsonBody) + } } val webhookCode = webhookResponse.code() if (webhookCode >= 400) { - println("Webhook error: HTTP $webhookCode") - println(webhookResponse.body()?.string()) + println("Webhook error: HTTP $webhookCode") + println(webhookResponse.body()?.string()) - exitProcess(1) + exitProcess(1) } exitProcess(0) diff --git a/.github/tag.main.kts b/.github/tag.main.kts index 6832786616..7fa3f91d54 100644 --- a/.github/tag.main.kts +++ b/.github/tag.main.kts @@ -10,66 +10,66 @@ val repo = System.getenv("GITHUB_REPOSITORY") var githubTag: String = System.getenv("GITHUB_REF") ?: error("No tag found in GITHUB_REF env var") if (githubTag.contains("/")) { - githubTag = githubTag.split("/").last() + githubTag = githubTag.split("/").last() } println("Current tag: $githubTag") if (githubTag.contains("v")) { - githubTag = githubTag.split("v", limit = 2).last() + githubTag = githubTag.split("v", limit = 2).last() } val tags = shellRun("git", listOf("tag")).trim().split("\n") val commits = if (tags.size < 2) { - println("No previous tags, using all branch commits.") + println("No previous tags, using all branch commits.") - shellRun("git", listOf("log", "--format=oneline", "--no-color")) + shellRun("git", listOf("log", "--format=oneline", "--no-color")) } else { - val previousTag = tags.takeLast(2).first() + val previousTag = tags.takeLast(2).first() - println("Previous tag: $previousTag") + println("Previous tag: $previousTag") - shellRun("git", listOf("log", "--format=oneline", "--no-color", "$previousTag..HEAD")) + shellRun("git", listOf("log", "--format=oneline", "--no-color", "$previousTag..HEAD")) }.split("\n").map { - val split = it.split(" ", limit = 2) - val commit = split.first() - val message = split.last() + val split = it.split(" ", limit = 2) + val commit = split.first() + val message = split.last() - Pair(commit, message) + Pair(commit, message) } println("Commits: ${commits.size}") val commitList = if (commits.size > 10) { - commits.take(10).joinToString("\n") { - val (commit, message) = it + commits.take(10).joinToString("\n") { + val (commit, message) = it - "* [${commit.take(6)}](https://github.com/$repo/commit/$commit): $message" - } + "\n\n...and ${commits.size - 10} more." + "* [${commit.take(6)}](https://github.com/$repo/commit/$commit): $message" + } + "\n\n...and ${commits.size - 10} more." } else { - commits.joinToString("\n") { - val (commit, message) = it + commits.joinToString("\n") { + val (commit, message) = it - "* [${commit.take(6)}](https://github.com/$repo/commit/$commit): $message" - } + "* [${commit.take(6)}](https://github.com/$repo/commit/$commit): $message" + } } val descFile = File("changes/$githubTag.md") val description = if (descFile.exists()) { - descFile.readText(Charsets.UTF_8).trim() + descFile.readText(Charsets.UTF_8).trim() } else { - "Description file `changes/$githubTag.md` not found - this release will need to be updated later!" + "Description file `changes/$githubTag.md` not found - this release will need to be updated later!" } val file = File("release.md") file.writeText( - "$description\n\n" + - "---\n\n" + - "# Commits (${commits.size}) \n\n" + - commitList + "$description\n\n" + + "---\n\n" + + "# Commits (${commits.size}) \n\n" + + commitList ) print("File written: release.md") diff --git a/.vscode/extensions.json b/.vscode/extensions.json index e997e2ed2c..3672951cdb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -12,6 +12,5 @@ ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [ - ] } diff --git a/annotation-processor/build.gradle.kts b/annotation-processor/build.gradle.kts index 3ba7837469..147dc53e86 100644 --- a/annotation-processor/build.gradle.kts +++ b/annotation-processor/build.gradle.kts @@ -1,30 +1,30 @@ plugins { - `kordex-module` - `published-module` + `kordex-module` + `published-module` } metadata { - name = "KordEx: Annotation Processor" - description = "KSP-based annotation processor designed for KordEx converters and plugins" + name = "KordEx: Annotation Processor" + description = "KSP-based annotation processor designed for KordEx converters and plugins" } dependencies { - implementation(libs.kotlin.stdlib) + implementation(libs.kotlin.stdlib) - implementation(libs.koin.core) - implementation(libs.ksp) + implementation(libs.koin.core) + implementation(libs.ksp) - implementation(project(":annotations")) + implementation(project(":annotations")) - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) } dokkaModule { - moduleName.set("Kord Extensions: Annotation Processor") + moduleName.set("Kord Extensions: Annotation Processor") } java { - sourceCompatibility = JavaVersion.VERSION_13 - targetCompatibility = JavaVersion.VERSION_13 + sourceCompatibility = JavaVersion.VERSION_13 + targetCompatibility = JavaVersion.VERSION_13 } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/ConverterProcessorProvider.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/ConverterProcessorProvider.kt index 07a6b10760..05f512d117 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/ConverterProcessorProvider.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/ConverterProcessorProvider.kt @@ -20,15 +20,15 @@ import org.koin.dsl.module * Processor provider for the converter annotation processor. */ public class ConverterProcessorProvider : SymbolProcessorProvider { - override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { - startKoin { - modules() - } + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + startKoin { + modules() + } - loadKoinModules(module { single { environment.logger } bind KSPLogger::class }) + loadKoinModules(module { single { environment.logger } bind KSPLogger::class }) - return ConverterProcessor( - environment.codeGenerator, environment.logger - ) - } + return ConverterProcessor( + environment.codeGenerator, environment.logger + ) + } } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/PluginProcessorProvider.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/PluginProcessorProvider.kt index 585f0a406d..9136f85799 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/PluginProcessorProvider.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/PluginProcessorProvider.kt @@ -15,9 +15,9 @@ import com.kotlindiscord.kord.extensions.modules.annotations.plugins.PluginProce * Processor provider for the converter annotation processor. */ public class PluginProcessorProvider : SymbolProcessorProvider { - override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { - return PluginProcessor( - environment.codeGenerator, environment.logger - ) - } + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return PluginProcessor( + environment.codeGenerator, environment.logger + ) + } } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/_Utils.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/_Utils.kt index ab5d44d89f..a54b2be639 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/_Utils.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/_Utils.kt @@ -44,55 +44,55 @@ public fun String.line(line: String): String = "$this${line.prependIndent(" " public fun String.closeBrace(): String = "$this}" public fun String.maybe(bool: Boolean, callback: (String) -> String): String = if (bool) { - callback(this) + callback(this) } else { - this + this } public fun String.maybe(predicate: () -> Boolean, callback: (String) -> String): String = maybe(predicate(), callback) public fun Collection.containsAny(vararg items: T): Boolean { - items.forEach { - if (this.contains(it)) return true - } + items.forEach { + if (this.contains(it)) return true + } - return false + return false } public fun String?.orNull(): String? = - if (this.isNullOrEmpty()) { - null - } else { - this - } + if (this.isNullOrEmpty()) { + null + } else { + this + } // Credit: https://stackoverflow.com/a/59737650 public fun List.permutations(): Set> { - if (this.isEmpty()) { - return emptySet() - } - - val permutationInstructions = this.toSet() - .map { it to this.count { x -> x == it } } - .fold(listOf(setOf>())) { acc, (value, valueCount) -> - mutableListOf>>().apply { - for (set in acc) for (retainIndex in 0 until valueCount) add(set + (value to retainIndex)) - } - } - - return mutableSetOf>().also { outSet -> - for (instructionSet in permutationInstructions) { - outSet += this.toMutableList().apply { - for ((value, retainIndex) in instructionSet) { - repeat(retainIndex) { removeAt(indexOfFirst { it == value }) } - repeat(count { it == value } - 1) { removeAt(indexOfLast { it == value }) } - } - } - } - } + if (this.isEmpty()) { + return emptySet() + } + + val permutationInstructions = this.toSet() + .map { it to this.count { x -> x == it } } + .fold(listOf(setOf>())) { acc, (value, valueCount) -> + mutableListOf>>().apply { + for (set in acc) for (retainIndex in 0 until valueCount) add(set + (value to retainIndex)) + } + } + + return mutableSetOf>().also { outSet -> + for (instructionSet in permutationInstructions) { + outSet += this.toMutableList().apply { + for ((value, retainIndex) in instructionSet) { + repeat(retainIndex) { removeAt(indexOfFirst { it == value }) } + repeat(count { it == value } - 1) { removeAt(indexOfLast { it == value }) } + } + } + } + } } public fun KSNode.validateIgnoringFunctions(): Boolean = - this.validate { parent, current -> - !(parent is KSFunctionDeclaration && current is KSDeclaration) - } + this.validate { parent, current -> + !(parent is KSFunctionDeclaration && current is KSDeclaration) + } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterAnnotationArgs.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterAnnotationArgs.kt index 19c8ad9531..1d83e246f0 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterAnnotationArgs.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterAnnotationArgs.kt @@ -19,67 +19,67 @@ import com.kotlindiscord.kord.extensions.modules.annotations.orNull */ @Suppress("UNCHECKED_CAST") public data class ConverterAnnotationArgs(public val annotation: KSAnnotation) { - /** @suppress **/ - private val argMap: Map = - annotation.arguments - .associate { it.name?.getShortName() to it.value } - .filterKeys { it != null } - - /** @suppress **/ - public val names: ArrayList = - argMap["names"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val types: List = - (argMap["types"]!! as ArrayList).mapNotNull { - ConverterType.fromName(it.declaration.simpleName.asString()) - } - - /** @suppress **/ - public val imports: ArrayList = - argMap["imports"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val builderConstructorArguments: ArrayList = - argMap["builderConstructorArguments"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val builderGeneric: String? = - (argMap["builderGeneric"] as String).orNull() - - /** @suppress **/ - public val builderFields: ArrayList = - argMap["builderFields"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val builderBuildFunctionStatements: ArrayList = - argMap["builderBuildFunctionStatements"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val builderBuildFunctionPreStatements: ArrayList = - argMap["builderBuildFunctionPreStatements"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val builderExtraStatements: ArrayList = - argMap["builderExtraStatements"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val builderInitStatements: ArrayList = - argMap["builderInitStatements"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val builderSuffixedWhere: String? = - (argMap["builderSuffixedWhere"] as String).orNull() - - /** @suppress **/ - public val functionGeneric: String? = - (argMap["functionGeneric"] as String).orNull() - - /** @suppress **/ - public val functionBuilderArguments: ArrayList = - argMap["functionBuilderArguments"] as ArrayList? ?: arrayListOf() - - /** @suppress **/ - public val functionSuffixedWhere: String? = - (argMap["functionSuffixedWhere"] as String).orNull() + /** @suppress **/ + private val argMap: Map = + annotation.arguments + .associate { it.name?.getShortName() to it.value } + .filterKeys { it != null } + + /** @suppress **/ + public val names: ArrayList = + argMap["names"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val types: List = + (argMap["types"]!! as ArrayList).mapNotNull { + ConverterType.fromName(it.declaration.simpleName.asString()) + } + + /** @suppress **/ + public val imports: ArrayList = + argMap["imports"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val builderConstructorArguments: ArrayList = + argMap["builderConstructorArguments"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val builderGeneric: String? = + (argMap["builderGeneric"] as String).orNull() + + /** @suppress **/ + public val builderFields: ArrayList = + argMap["builderFields"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val builderBuildFunctionStatements: ArrayList = + argMap["builderBuildFunctionStatements"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val builderBuildFunctionPreStatements: ArrayList = + argMap["builderBuildFunctionPreStatements"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val builderExtraStatements: ArrayList = + argMap["builderExtraStatements"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val builderInitStatements: ArrayList = + argMap["builderInitStatements"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val builderSuffixedWhere: String? = + (argMap["builderSuffixedWhere"] as String).orNull() + + /** @suppress **/ + public val functionGeneric: String? = + (argMap["functionGeneric"] as String).orNull() + + /** @suppress **/ + public val functionBuilderArguments: ArrayList = + argMap["functionBuilderArguments"] as ArrayList? ?: arrayListOf() + + /** @suppress **/ + public val functionSuffixedWhere: String? = + (argMap["functionSuffixedWhere"] as String).orNull() } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterFunctionBuilder.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterFunctionBuilder.kt index 292bed8c46..2d1aff8236 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterFunctionBuilder.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterFunctionBuilder.kt @@ -14,252 +14,252 @@ import org.koin.core.component.inject /** Convenience class for building converter functions. **/ public class ConverterFunctionBuilder( - public val name: String, + public val name: String, ) : KoinComponent { - private val logger: KSPLogger by inject() + private val logger: KSPLogger by inject() - private var comment: String? = null - private val functionArgs: MutableList = mutableListOf() + private var comment: String? = null + private val functionArgs: MutableList = mutableListOf() - private var generic: String? = null + private var generic: String? = null - private lateinit var converterName: String - private val converterArgs: MutableList = mutableListOf() + private lateinit var converterName: String + private val converterArgs: MutableList = mutableListOf() - private var wrapperName: String? = null - private val wrapperArgs: MutableList = mutableListOf() + private var wrapperName: String? = null + private val wrapperArgs: MutableList = mutableListOf() - private val lines: MutableList = mutableListOf() - private lateinit var returnType: String + private val lines: MutableList = mutableListOf() + private lateinit var returnType: String - private var implicitReturn: Boolean = true + private var implicitReturn: Boolean = true - public fun returnType(type: String): ConverterFunctionBuilder { - returnType = type + public fun returnType(type: String): ConverterFunctionBuilder { + returnType = type - return this - } + return this + } - public fun defaultFirstArgs(): ConverterFunctionBuilder { - requiredFunArg("displayName", "String") - requiredFunArg("description", "String") + public fun defaultFirstArgs(): ConverterFunctionBuilder { + requiredFunArg("displayName", "String") + requiredFunArg("description", "String") - return this - } + return this + } - public fun defaultLastArgs(typeParam: String): ConverterFunctionBuilder { - if (generic != null) { - optionalFunArg("noinline validator", "Validator<$typeParam>", "null") - } else { - optionalFunArg("validator", "Validator<$typeParam>", "null") - } + public fun defaultLastArgs(typeParam: String): ConverterFunctionBuilder { + if (generic != null) { + optionalFunArg("noinline validator", "Validator<$typeParam>", "null") + } else { + optionalFunArg("validator", "Validator<$typeParam>", "null") + } - return this - } + return this + } - public fun comment(lines: String): ConverterFunctionBuilder { - comment = lines + public fun comment(lines: String): ConverterFunctionBuilder { + comment = lines - return this - } + return this + } - public fun converter(name: String): ConverterFunctionBuilder { - converterName = name + public fun converter(name: String): ConverterFunctionBuilder { + converterName = name - return this - } + return this + } - public fun converterArg(name: String): ConverterFunctionBuilder { - converterArgs.add("$name = $name") + public fun converterArg(name: String): ConverterFunctionBuilder { + converterArgs.add("$name = $name") - return this - } + return this + } - public fun converterArg(name: String, value: String): ConverterFunctionBuilder { - converterArgs.add("$name = $value") + public fun converterArg(name: String, value: String): ConverterFunctionBuilder { + converterArgs.add("$name = $value") - return this - } + return this + } - public fun rawGeneric(generic: String): ConverterFunctionBuilder { - this.generic = generic + public fun rawGeneric(generic: String): ConverterFunctionBuilder { + this.generic = generic - return this - } + return this + } - public fun generic(name: String, type: String): ConverterFunctionBuilder { - generic = "$name : $type" + public fun generic(name: String, type: String): ConverterFunctionBuilder { + generic = "$name : $type" - return this - } + return this + } - public fun wrapper(name: String): ConverterFunctionBuilder { - wrapperName = name + public fun wrapper(name: String): ConverterFunctionBuilder { + wrapperName = name - return this - } + return this + } - public fun wrapperArg(name: String): ConverterFunctionBuilder { - wrapperArgs.add("$name = $name") + public fun wrapperArg(name: String): ConverterFunctionBuilder { + wrapperArgs.add("$name = $name") - return this - } + return this + } - public fun wrapperArg(name: String, value: String): ConverterFunctionBuilder { - wrapperArgs.add("$name = $value") + public fun wrapperArg(name: String, value: String): ConverterFunctionBuilder { + wrapperArgs.add("$name = $value") - return this - } + return this + } - public fun rawFunArg(line: String): ConverterFunctionBuilder { - functionArgs.add(line.trimEnd(',')) + public fun rawFunArg(line: String): ConverterFunctionBuilder { + functionArgs.add(line.trimEnd(',')) - return this - } + return this + } - public fun requiredFunArg(name: String, type: String): ConverterFunctionBuilder { - functionArgs.add("$name: $type") + public fun requiredFunArg(name: String, type: String): ConverterFunctionBuilder { + functionArgs.add("$name: $type") - return this - } + return this + } - public fun optionalFunArg(name: String, type: String, default: String): ConverterFunctionBuilder { - functionArgs.add("$name: $type = $default") + public fun optionalFunArg(name: String, type: String, default: String): ConverterFunctionBuilder { + functionArgs.add("$name: $type = $default") - return this - } + return this + } - public fun explicitReturn(): ConverterFunctionBuilder { - implicitReturn = false + public fun explicitReturn(): ConverterFunctionBuilder { + implicitReturn = false - return this - } + return this + } - public fun line(line: String): ConverterFunctionBuilder { - lines.add(line) + public fun line(line: String): ConverterFunctionBuilder { + lines.add(line) - return this - } + return this + } - public fun maybe(bool: Boolean, callback: ConverterFunctionBuilder.() -> Unit): ConverterFunctionBuilder { - if (bool) { - callback(this) - } + public fun maybe(bool: Boolean, callback: ConverterFunctionBuilder.() -> Unit): ConverterFunctionBuilder { + if (bool) { + callback(this) + } - return this - } + return this + } - public fun maybe( - predicate: () -> Boolean, - callback: ConverterFunctionBuilder.() -> Unit - ): ConverterFunctionBuilder = maybe(predicate(), callback) + public fun maybe( + predicate: () -> Boolean, + callback: ConverterFunctionBuilder.() -> Unit, + ): ConverterFunctionBuilder = maybe(predicate(), callback) - public fun build(): String { - var result = buildString { - if (comment != null) { - append( - """ + public fun build(): String { + val result = buildString { + if (comment != null) { + append( + """ |/** ${comment!!.split("\n").joinToString("\n") { "| * $it" }} | */ """.trimMargin() + "\n" - ) - } - - append("public ") - - if (generic != null) { - append("inline ") - } - - append("fun ") - - if (generic != null) { - append(" ") - } - - append("Arguments.$name(\n") - append(functionArgs.joinToString("") { " $it,\n" }) - append("): $returnType ") - - if (implicitReturn) { - append("=") - } else { - append("{") - } - - append("\n") - - if (lines.isNotEmpty()) { - append(lines.joinToString("") { " $it\n" }) - append("\n") - } - - append(" arg(\n") - append(" displayName = displayName,\n") - append(" description = description,\n") - append("\n") - append(" converter = $converterName(") - - if (converterArgs.isNotEmpty()) { - append("\n") - append(converterArgs.joinToString("") { " $it,\n" }) - append(" ") - } - - append(")") - - if (converterArgs.isNotEmpty() && wrapperName != null) { - append("") - } else if (converterArgs.isEmpty() && wrapperName != null) { - append("\n ") - } else { - append("\n") - } - - if (wrapperName != null) { - append(".to${wrapperName!!.toCapitalized()}(") - - logger.info("== Wrapper args ==\n ${wrapperArgs.joinToString(", ") { "\"$it\"" }}\n") - - if (wrapperArgs.isNotEmpty()) { - append("\n") - - if (converterArgs.isNotEmpty()) { - append( - wrapperArgs.joinToString("") { " $it,\n" } + - " " - ) - } else { - append( - wrapperArgs.joinToString("") { " $it,\n" } + - " " - ) - } - } - - append(")\n") - } - - append(" )\n") - - if (!implicitReturn) { - append("}") - } - } - - return result - } + ) + } + + append("public ") + + if (generic != null) { + append("inline ") + } + + append("fun ") + + if (generic != null) { + append(" ") + } + + append("Arguments.$name(\n") + append(functionArgs.joinToString("") { " $it,\n" }) + append("): $returnType ") + + if (implicitReturn) { + append("=") + } else { + append("{") + } + + append("\n") + + if (lines.isNotEmpty()) { + append(lines.joinToString("") { " $it\n" }) + append("\n") + } + + append(" arg(\n") + append(" displayName = displayName,\n") + append(" description = description,\n") + append("\n") + append(" converter = $converterName(") + + if (converterArgs.isNotEmpty()) { + append("\n") + append(converterArgs.joinToString("") { " $it,\n" }) + append(" ") + } + + append(")") + + if (converterArgs.isNotEmpty() && wrapperName != null) { + append("") + } else if (converterArgs.isEmpty() && wrapperName != null) { + append("\n ") + } else { + append("\n") + } + + if (wrapperName != null) { + append(".to${wrapperName!!.toCapitalized()}(") + + logger.info("== Wrapper args ==\n ${wrapperArgs.joinToString(", ") { "\"$it\"" }}\n") + + if (wrapperArgs.isNotEmpty()) { + append("\n") + + if (converterArgs.isNotEmpty()) { + append( + wrapperArgs.joinToString("") { " $it,\n" } + + " " + ) + } else { + append( + wrapperArgs.joinToString("") { " $it,\n" } + + " " + ) + } + } + + append(")\n") + } + + append(" )\n") + + if (!implicitReturn) { + append("}") + } + } + + return result + } } @Suppress("FunctionNaming") // Factory function public fun ConverterFunctionBuilder( - name: String, - body: ConverterFunctionBuilder.() -> Unit + name: String, + body: ConverterFunctionBuilder.() -> Unit, ): String { - val builder = ConverterFunctionBuilder(name = name) + val builder = ConverterFunctionBuilder(name = name) - body(builder) + body(builder) - return builder.build() + return builder.build() } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterProcessor.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterProcessor.kt index 465c5293ee..67cf5a6730 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterProcessor.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterProcessor.kt @@ -21,139 +21,139 @@ import java.util.* * Annotation processor for KordEx converters. */ public class ConverterProcessor( - private val generator: CodeGenerator, - private val logger: KSPLogger, + private val generator: CodeGenerator, + private val logger: KSPLogger, ) : SymbolProcessor { - override fun process(resolver: Resolver): List { - val symbols = resolver.getSymbolsWithAnnotation( - "com.kotlindiscord.kord.extensions.modules.annotations.converters.Converter" - ) - - val ret = symbols.filter { !it.validate() }.toList() - - ret.forEach { - logger.warn("Unable to validate: $it") - } - - symbols.filter { it is KSClassDeclaration && it.validate() } - .forEach { it.accept(ConverterVisitor(), Unit) } - - return ret - } - - /** Converter annotation visitor. **/ - public inner class ConverterVisitor : KSVisitorVoid() { - override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { - val annotation = classDeclaration.annotations.filter { - it.annotationType.resolve().declaration.qualifiedName?.asString() == - "com.kotlindiscord.kord.extensions.modules.annotations.converters.Converter" - }.first() - - logger.info("Found annotation", annotation) - - val arguments = ConverterAnnotationArgs(annotation) - - logger.info( - "Arguments: \n" + annotation.arguments.joinToString("\n") { - " ${it.name?.getShortName()} : ${it.value}" - } - ) - - val superTypes = classDeclaration.superTypes.map { it.resolve() }.toList() - - logger.info( - "Super types (${superTypes.size}): " + superTypes.joinToString(", ") { - it.declaration.simpleName.asString() - } - ) - - val superType = superTypes.first() - val typeParams = superType.arguments - - logger.info( - "Types params (${typeParams.size}): " + typeParams.joinToString(", ") { - it.type!!.resolve().declaration.simpleName.asString() - } - ) - - val typeVars = typeParams.map { it.type!!.resolve().declaration } - val firstTypeVar = typeVars.first() - - val typeVarName = buildString { - append(firstTypeVar.simpleName.asString()) - - if (firstTypeVar.typeParameters.isNotEmpty()) { - append("<") - - append( - firstTypeVar.typeParameters.joinToString { - it.bounds.joinToString { bound -> bound.resolve().declaration.simpleName.asString() } - } - ) - - append(">") - } - } - - val strings: MutableList = mutableListOf() - - for (name in arguments.names) { - if (arguments.types.count { type -> type.order == 1 } != 1) { - error( - "Types list must contain exactly one of COALESCING or SINGLE. Converter: " + - classDeclaration.simpleName.asString() - ) - } - - val primaryType = arguments.types.first { type -> type.order == 1 } // Order 1 is single/coalescing - val isChoice = ConverterType.CHOICE in arguments.types - - val baseTypes = arguments.types - .filter { it.order == 0 } - - strings.add( - generate( - classDeclaration, - arguments, - name, - typeVarName, - - if (isChoice) { - listOf(primaryType, ConverterType.CHOICE) - } else { - listOf(primaryType) - } - ) - ) - - baseTypes.forEach { type -> - strings.add( - generate( - classDeclaration, - arguments, - name, - typeVarName, - - if (isChoice) { - listOf(primaryType, type, ConverterType.CHOICE) - } else { - listOf(primaryType, type) - } - ) - ) - } - } - - val ignoredGenerics = listOfNotNull( - arguments.builderGeneric?.split(":")?.first(), - arguments.functionGeneric?.split(":")?.first() - ) - - val typeImports = typeVars.filter { it.simpleName.getShortName() !in ignoredGenerics } - - val outputText = buildString { - append( - """ + override fun process(resolver: Resolver): List { + val symbols = resolver.getSymbolsWithAnnotation( + "com.kotlindiscord.kord.extensions.modules.annotations.converters.Converter" + ) + + val ret = symbols.filter { !it.validate() }.toList() + + ret.forEach { + logger.warn("Unable to validate: $it") + } + + symbols.filter { it is KSClassDeclaration && it.validate() } + .forEach { it.accept(ConverterVisitor(), Unit) } + + return ret + } + + /** Converter annotation visitor. **/ + public inner class ConverterVisitor : KSVisitorVoid() { + override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { + val annotation = classDeclaration.annotations.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == + "com.kotlindiscord.kord.extensions.modules.annotations.converters.Converter" + }.first() + + logger.info("Found annotation", annotation) + + val arguments = ConverterAnnotationArgs(annotation) + + logger.info( + "Arguments: \n" + annotation.arguments.joinToString("\n") { + " ${it.name?.getShortName()} : ${it.value}" + } + ) + + val superTypes = classDeclaration.superTypes.map { it.resolve() }.toList() + + logger.info( + "Super types (${superTypes.size}): " + superTypes.joinToString(", ") { + it.declaration.simpleName.asString() + } + ) + + val superType = superTypes.first() + val typeParams = superType.arguments + + logger.info( + "Types params (${typeParams.size}): " + typeParams.joinToString(", ") { + it.type!!.resolve().declaration.simpleName.asString() + } + ) + + val typeVars = typeParams.map { it.type!!.resolve().declaration } + val firstTypeVar = typeVars.first() + + val typeVarName = buildString { + append(firstTypeVar.simpleName.asString()) + + if (firstTypeVar.typeParameters.isNotEmpty()) { + append("<") + + append( + firstTypeVar.typeParameters.joinToString { + it.bounds.joinToString { bound -> bound.resolve().declaration.simpleName.asString() } + } + ) + + append(">") + } + } + + val strings: MutableList = mutableListOf() + + for (name in arguments.names) { + if (arguments.types.count { type -> type.order == 1 } != 1) { + error( + "Types list must contain exactly one of COALESCING or SINGLE. Converter: " + + classDeclaration.simpleName.asString() + ) + } + + val primaryType = arguments.types.first { type -> type.order == 1 } // Order 1 is single/coalescing + val isChoice = ConverterType.CHOICE in arguments.types + + val baseTypes = arguments.types + .filter { it.order == 0 } + + strings.add( + generate( + classDeclaration, + arguments, + name, + typeVarName, + + if (isChoice) { + listOf(primaryType, ConverterType.CHOICE) + } else { + listOf(primaryType) + } + ) + ) + + baseTypes.forEach { type -> + strings.add( + generate( + classDeclaration, + arguments, + name, + typeVarName, + + if (isChoice) { + listOf(primaryType, type, ConverterType.CHOICE) + } else { + listOf(primaryType, type) + } + ) + ) + } + } + + val ignoredGenerics = listOfNotNull( + arguments.builderGeneric?.split(":")?.first(), + arguments.functionGeneric?.split(":")?.first() + ) + + val typeImports = typeVars.filter { it.simpleName.getShortName() !in ignoredGenerics } + + val outputText = buildString { + append( + """ @file:OptIn( KordPreview::class, ConverterToDefaulting::class, @@ -173,104 +173,104 @@ public class ConverterProcessor( import com.kotlindiscord.kord.extensions.commands.converters.builders.* import dev.kord.common.annotation.KordPreview """.trimIndent() - ) + ) - if (typeImports.isNotEmpty()) { - append("\n\n// Converter type params") + if (typeImports.isNotEmpty()) { + append("\n\n// Converter type params") - typeImports.forEach { - append("\nimport ${it.qualifiedName!!.asString()}") - } - } + typeImports.forEach { + append("\nimport ${it.qualifiedName!!.asString()}") + } + } - append("\n\n") + append("\n\n") - if (arguments.imports.isNotEmpty()) { - append("// Extra imports\n") - append(arguments.imports.joinToString("\n") { "import $it" }) - append("\n\n") - } + if (arguments.imports.isNotEmpty()) { + append("// Extra imports\n") + append(arguments.imports.joinToString("\n") { "import $it" }) + append("\n\n") + } - append(strings.filter { it.isNotEmpty() }.joinToString("\n\n")) + append(strings.filter { it.isNotEmpty() }.joinToString("\n\n")) - append("\n") - } + append("\n") + } - val file = generator.createNewFile( - Dependencies(true, classDeclaration.containingFile!!), - classDeclaration.packageName.asString(), - classDeclaration.simpleName.asString() + "Functions" - ) + val file = generator.createNewFile( + Dependencies(true, classDeclaration.containingFile!!), + classDeclaration.packageName.asString(), + classDeclaration.simpleName.asString() + "Functions" + ) - file.write(outputText.encodeToByteArray()) - file.flush() - file.close() - } - } + file.write(outputText.encodeToByteArray()) + file.flush() + file.close() + } + } - internal fun generate( - classDeclaration: KSClassDeclaration, - arguments: ConverterAnnotationArgs, - converterName: String, - argumentTypeString: String, - types: List, - ): String { - val classBuilder = builderClass { - comment = classComment(converterName, classDeclaration.simpleName.asString()) + internal fun generate( + classDeclaration: KSClassDeclaration, + arguments: ConverterAnnotationArgs, + converterName: String, + argumentTypeString: String, + types: List, + ): String { + val classBuilder = builderClass { + comment = classComment(converterName, classDeclaration.simpleName.asString()) - name = converterName.toCapitalized() - converterClass = classDeclaration.simpleName.asString() - argumentType = argumentTypeString + name = converterName.toCapitalized() + converterClass = classDeclaration.simpleName.asString() + argumentType = argumentTypeString - builderGeneric = arguments.builderGeneric + builderGeneric = arguments.builderGeneric - arguments.builderConstructorArguments.forEach(this::builderArg) - arguments.builderFields.forEach(this::builderField) - arguments.builderBuildFunctionPreStatements.forEach(this::builderBuildFunctionPreStatement) - arguments.builderBuildFunctionStatements.forEach(this::builderBuildFunctionStatement) - arguments.builderExtraStatements.forEach(this::builderExtraStatement) - arguments.builderInitStatements.forEach(this::builderInitStatement) + arguments.builderConstructorArguments.forEach(this::builderArg) + arguments.builderFields.forEach(this::builderField) + arguments.builderBuildFunctionPreStatements.forEach(this::builderBuildFunctionPreStatement) + arguments.builderBuildFunctionStatements.forEach(this::builderBuildFunctionStatement) + arguments.builderExtraStatements.forEach(this::builderExtraStatement) + arguments.builderInitStatements.forEach(this::builderInitStatement) - whereSuffix = arguments.builderSuffixedWhere + whereSuffix = arguments.builderSuffixedWhere - types(types) - } + types(types) + } - val function = builderFunction { - comment = functionComment( - converterName, - classBuilder.converterType.splitUpper().joinToString(" ") { it.toLowered() }, - classBuilder.builderType - ) + val function = builderFunction { + comment = functionComment( + converterName, + classBuilder.converterType.splitUpper().joinToString(" ") { it.toLowered() }, + classBuilder.builderType + ) - name = classBuilder.getFunctionName(converterName) + name = classBuilder.getFunctionName(converterName) - builderGeneric = arguments.builderGeneric - functionGeneric = arguments.functionGeneric + builderGeneric = arguments.builderGeneric + functionGeneric = arguments.functionGeneric - argumentType = argumentTypeString - builderType = classBuilder.builderType - converterType = classBuilder.converterType + argumentType = argumentTypeString + builderType = classBuilder.builderType + converterType = classBuilder.converterType - whereSuffix = arguments.functionSuffixedWhere + whereSuffix = arguments.functionSuffixedWhere - arguments.functionBuilderArguments.forEach(this::builderArg) - } + arguments.functionBuilderArguments.forEach(this::builderArg) + } - return """ + return """ |${classBuilder.result!!.trim('\n', ' ')} |${function.trim('\n', ' ')} """.trimMargin().trim('\n', ' ') - } + } - internal fun classComment(name: String, see: String): String = """ + internal fun classComment(name: String, see: String): String = """ Builder class for $name converters. Used to construct a converter based on the given options. @see $see """.trimIndent() - internal fun functionComment(name: String, type: String, see: String): String = """ + internal fun functionComment(name: String, type: String, see: String): String = """ Converter creation function: $name $type @see $see @@ -278,36 +278,36 @@ public class ConverterProcessor( } internal fun String.toCapitalized() = - replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } internal fun String.toLowered() = - replaceFirstChar { if (it.isUpperCase()) it.lowercase(Locale.getDefault()) else it.toString() } + replaceFirstChar { if (it.isUpperCase()) it.lowercase(Locale.getDefault()) else it.toString() } internal fun String.splitUpper(): List { - val parts: MutableList = mutableListOf() + val parts: MutableList = mutableListOf() - val currentPart = buildString { - for (char in this@splitUpper) { - if (isEmpty()) { - append(char) - continue - } + val currentPart = buildString { + for (char in this@splitUpper) { + if (isEmpty()) { + append(char) + continue + } - if (char.isUpperCase()) { - parts.add(this.toString()) - this.clear() + if (char.isUpperCase()) { + parts.add(this.toString()) + this.clear() - append("" + char) - continue - } + append("" + char) + continue + } - append(char) - } - } + append(char) + } + } - if (currentPart.isNotEmpty()) { - parts.add(currentPart) - } + if (currentPart.isNotEmpty()) { + parts.add(currentPart) + } - return parts.toList() + return parts.toList() } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/Functions.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/Functions.kt index e417281195..f590d8794b 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/Functions.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/Functions.kt @@ -11,488 +11,488 @@ package com.kotlindiscord.kord.extensions.modules.annotations.converters import com.google.devtools.ksp.symbol.KSClassDeclaration internal fun defaultingConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "defaulting${name.toCapitalized()}" + "defaulting${name.toCapitalized()}" ).comment( - """ + """ Creates a defaulting $name converter, for single arguments. @param defaultValue Default value to use if no argument was provided. @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("DefaultingConverter<$typeParam>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "DefaultingConverter<${generic.split(":").first().trim()}>" - ) - } - .defaultFirstArgs() - .requiredFunArg("defaultValue", typeParam) - .optionalFunArg("required", "Boolean", "false") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim()) - }.maybe(generic == null) { - defaultLastArgs(typeParam) - } - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .wrapper("defaulting") - .wrapperArg("defaultValue") - .wrapperArg("outputError", "required") - .wrapperArg("nestedValidator", "validator") - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("DefaultingConverter<$typeParam>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "DefaultingConverter<${generic.split(":").first().trim()}>" + ) + } + .defaultFirstArgs() + .requiredFunArg("defaultValue", typeParam) + .optionalFunArg("required", "Boolean", "false") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim()) + }.maybe(generic == null) { + defaultLastArgs(typeParam) + } + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .wrapper("defaulting") + .wrapperArg("defaultValue") + .wrapperArg("outputError", "required") + .wrapperArg("nestedValidator", "validator") + .build() internal fun defaultingChoiceConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "defaulting${name.toCapitalized()}Choice", + "defaulting${name.toCapitalized()}Choice", ).comment( - """ + """ Creates a defaulting $name choice converter, for a defined set of single arguments. @param defaultValue Default value to use if no argument was provided. @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("DefaultingConverter<$typeParam>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "DefaultingConverter<<${generic.split(":").first().trim()}>>" - ) - } - .defaultFirstArgs() - .requiredFunArg("defaultValue", typeParam) - .optionalFunArg("required", "Boolean", "false") - .requiredFunArg("choices", "Map") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim()) - }.maybe(generic == null) { - defaultLastArgs(typeParam) - } - .converterArg("choices") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .wrapper("defaulting") - .wrapperArg("defaultValue") - .wrapperArg("outputError", "required") - .wrapperArg("nestedValidator", "validator") - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("DefaultingConverter<$typeParam>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "DefaultingConverter<<${generic.split(":").first().trim()}>>" + ) + } + .defaultFirstArgs() + .requiredFunArg("defaultValue", typeParam) + .optionalFunArg("required", "Boolean", "false") + .requiredFunArg("choices", "Map") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim()) + }.maybe(generic == null) { + defaultLastArgs(typeParam) + } + .converterArg("choices") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .wrapper("defaulting") + .wrapperArg("defaultValue") + .wrapperArg("outputError", "required") + .wrapperArg("nestedValidator", "validator") + .build() internal fun defaultingCoalescingConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "defaultingCoalescing${name.toCapitalized()}", + "defaultingCoalescing${name.toCapitalized()}", ).comment( - """ + """ Creates a defaulting coalescing $name converter. @param defaultValue Default value to use if no argument was provided. @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("DefaultingCoalescingConverter<$typeParam>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "DefaultingCoalescingConverter<${generic.split(":").first().trim()}>" - ) - } - .defaultFirstArgs() - .requiredFunArg("defaultValue", typeParam) - .optionalFunArg("required", "Boolean", "false") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim()) - }.maybe(generic == null) { - defaultLastArgs(typeParam) - } - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .wrapper("defaulting") - .wrapperArg("defaultValue") - .wrapperArg("outputError", "required") - .wrapperArg("nestedValidator", "validator") - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("DefaultingCoalescingConverter<$typeParam>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "DefaultingCoalescingConverter<${generic.split(":").first().trim()}>" + ) + } + .defaultFirstArgs() + .requiredFunArg("defaultValue", typeParam) + .optionalFunArg("required", "Boolean", "false") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim()) + }.maybe(generic == null) { + defaultLastArgs(typeParam) + } + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .wrapper("defaulting") + .wrapperArg("defaultValue") + .wrapperArg("outputError", "required") + .wrapperArg("nestedValidator", "validator") + .build() internal fun listConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "${name.toLowered()}List" + "${name.toLowered()}List" ).comment( - """ + """ Creates a $name converter, for lists of arguments. - + @param required Whether command parsing should fail if no arguments could be converted. @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("MultiConverter<$typeParam>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "MultiConverter<${generic.split(":").first().trim()}>" - ) - } - .defaultFirstArgs() - .optionalFunArg("required", "Boolean", "true") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs("List<${generic!!.split(":").first().trim()}>") - }.maybe(generic == null) { - defaultLastArgs("List<$typeParam>") - } - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .wrapper("multi") - .wrapperArg("required") - .wrapperArg("nestedValidator", "validator") - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("MultiConverter<$typeParam>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "MultiConverter<${generic.split(":").first().trim()}>" + ) + } + .defaultFirstArgs() + .optionalFunArg("required", "Boolean", "true") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs("List<${generic!!.split(":").first().trim()}>") + }.maybe(generic == null) { + defaultLastArgs("List<$typeParam>") + } + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .wrapper("multi") + .wrapperArg("required") + .wrapperArg("nestedValidator", "validator") + .build() internal fun listChoiceConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = error("Choice converters are incompatible with list converters.") internal fun listCoalescingConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = error("Coalescing converters are incompatible with list converters.") internal fun singleConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - name.toLowered() + name.toLowered() ).comment( - """ + """ Creates a $name converter, for single arguments. - + @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("SingleConverter<$typeParam>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "SingleConverter<${generic.split(":").first().trim()}>" - ) - } - .defaultFirstArgs() - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim()) - }.maybe(generic == null) { - defaultLastArgs(typeParam) - } - .converterArg("validator") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("SingleConverter<$typeParam>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "SingleConverter<${generic.split(":").first().trim()}>" + ) + } + .defaultFirstArgs() + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim()) + }.maybe(generic == null) { + defaultLastArgs(typeParam) + } + .converterArg("validator") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .build() internal fun singleChoiceConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "${name.toLowered()}Choice" + "${name.toLowered()}Choice" ).comment( - """ + """ Creates a $name choice converter, for a defined set of single arguments. - + @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("SingleConverter<$typeParam>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "SingleConverter<${generic.split(":").first().trim()}>" - ) - } - .defaultFirstArgs() - .requiredFunArg("choices", "Map") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim()) - }.maybe(generic == null) { - defaultLastArgs(typeParam) - } - .converterArg("choices") - .converterArg("validator") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("SingleConverter<$typeParam>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "SingleConverter<${generic.split(":").first().trim()}>" + ) + } + .defaultFirstArgs() + .requiredFunArg("choices", "Map") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim()) + }.maybe(generic == null) { + defaultLastArgs(typeParam) + } + .converterArg("choices") + .converterArg("validator") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .build() internal fun singleCoalescingConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "coalesced${name.toCapitalized()}", + "coalesced${name.toCapitalized()}", ).comment( - """ + """ Creates a coalescing $name converter. @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("CoalescingConverter<$typeParam>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "CoalescingConverter<${generic.split(":").first().trim()}>" - ) - } - .defaultFirstArgs() - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim()) - }.maybe(generic == null) { - defaultLastArgs(typeParam) - } - .converterArg("validator") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("CoalescingConverter<$typeParam>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "CoalescingConverter<${generic.split(":").first().trim()}>" + ) + } + .defaultFirstArgs() + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim()) + }.maybe(generic == null) { + defaultLastArgs(typeParam) + } + .converterArg("validator") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .build() internal fun optionalConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "optional${name.toCapitalized()}", + "optional${name.toCapitalized()}", ).comment( - """ + """ Creates an optional $name converter, for single arguments. - + @param required Whether command parsing should fail if an invalid argument is provided. @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("OptionalConverter<$typeParam?>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "OptionalConverter<${generic.split(":").first().trim()}?>" - ) - } - .defaultFirstArgs() - .optionalFunArg("required", "Boolean", "false") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim() + "?") - } - .maybe(generic == null) { - defaultLastArgs("$typeParam?") - } - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .wrapper("optional") - .wrapperArg("outputError", "required") - .wrapperArg("nestedValidator", "validator") - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("OptionalConverter<$typeParam?>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "OptionalConverter<${generic.split(":").first().trim()}?>" + ) + } + .defaultFirstArgs() + .optionalFunArg("required", "Boolean", "false") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim() + "?") + } + .maybe(generic == null) { + defaultLastArgs("$typeParam?") + } + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .wrapper("optional") + .wrapperArg("outputError", "required") + .wrapperArg("nestedValidator", "validator") + .build() internal fun optionalChoiceConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "optional${name.toCapitalized()}Choice", + "optional${name.toCapitalized()}Choice", ).comment( - """ + """ Creates an optional $name choice converter, for a defined set of single arguments. - + @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("OptionalConverter<$typeParam?>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "OptionalConverter<${generic.split(":").first().trim()}?>" - ) - } - .defaultFirstArgs() - .requiredFunArg("choices", "Map") - .optionalFunArg("required", "Boolean", "false") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim() + "?") - } - .maybe(generic == null) { - defaultLastArgs("$typeParam?") - } - .converterArg("choices") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .wrapper("optional") - .wrapperArg("outputError", "required") - .wrapperArg("nestedValidator", "validator") - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("OptionalConverter<$typeParam?>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "OptionalConverter<${generic.split(":").first().trim()}?>" + ) + } + .defaultFirstArgs() + .requiredFunArg("choices", "Map") + .optionalFunArg("required", "Boolean", "false") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim() + "?") + } + .maybe(generic == null) { + defaultLastArgs("$typeParam?") + } + .converterArg("choices") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .wrapper("optional") + .wrapperArg("outputError", "required") + .wrapperArg("nestedValidator", "validator") + .build() internal fun optionalCoalescingConverter( - classDeclaration: KSClassDeclaration, - name: String, - typeParam: String, - extraArguments: ArrayList, - generic: String?, + classDeclaration: KSClassDeclaration, + name: String, + typeParam: String, + extraArguments: ArrayList, + generic: String?, ): String = ConverterFunctionBuilder( - "optionalCoalescing${name.toCapitalized()}", + "optionalCoalescing${name.toCapitalized()}", ).comment( - """ + """ Creates an optional coalescing $name converter. @see ${classDeclaration.simpleName.asString()} """.trimIndent() ) - .converter(classDeclaration.simpleName.asString()) - .returnType("OptionalCoalescingConverter<$typeParam?>") - .maybe(generic != null) { - rawGeneric(generic!!) - - returnType( - "OptionalCoalescingConverter<${generic.split(":").first().trim()}?>" - ) - } - .defaultFirstArgs() - .optionalFunArg("required", "Boolean", "false") - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - rawFunArg(it) - } - } - .maybe(generic != null) { - defaultLastArgs(generic!!.split(":").first().trim() + "?") - } - .maybe(generic == null) { - defaultLastArgs("$typeParam?") - } - .maybe(extraArguments.isNotEmpty()) { - extraArguments.forEach { - converterArg(it.split(":").first().trim().split(" ").last()) - } - } - .wrapper("optional") - .wrapperArg("outputError", "required") - .wrapperArg("nestedValidator", "validator") - .build() + .converter(classDeclaration.simpleName.asString()) + .returnType("OptionalCoalescingConverter<$typeParam?>") + .maybe(generic != null) { + rawGeneric(generic!!) + + returnType( + "OptionalCoalescingConverter<${generic.split(":").first().trim()}?>" + ) + } + .defaultFirstArgs() + .optionalFunArg("required", "Boolean", "false") + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + rawFunArg(it) + } + } + .maybe(generic != null) { + defaultLastArgs(generic!!.split(":").first().trim() + "?") + } + .maybe(generic == null) { + defaultLastArgs("$typeParam?") + } + .maybe(extraArguments.isNotEmpty()) { + extraArguments.forEach { + converterArg(it.split(":").first().trim().split(" ").last()) + } + } + .wrapper("optional") + .wrapperArg("outputError", "required") + .wrapperArg("nestedValidator", "validator") + .build() diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/builders/ConverterBuilderClassBuilder.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/builders/ConverterBuilderClassBuilder.kt index d0e3a2c31f..37ec213fe2 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/builders/ConverterBuilderClassBuilder.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/builders/ConverterBuilderClassBuilder.kt @@ -5,7 +5,7 @@ */ @file:Suppress( - "StringLiteralDuplication" + "StringLiteralDuplication" ) package com.kotlindiscord.kord.extensions.modules.annotations.converters.builders @@ -21,365 +21,365 @@ import org.koin.core.component.KoinComponent * This is a fairly messy class containing a string builder, but what can you do? */ public class ConverterBuilderClassBuilder : KoinComponent { - /** Comment to prepend to the class definition. **/ - public var comment: String? = null + /** Comment to prepend to the class definition. **/ + public var comment: String? = null - /** Converter name - eg, `"${name}ConverterBuilder"`. **/ - public lateinit var name: String + /** Converter name - eg, `"${name}ConverterBuilder"`. **/ + public lateinit var name: String - /** - * Converter class - class that should be instantiated to construct the converter. - * - * Technically could be a function, but this would be playing with fire. - */ - public lateinit var converterClass: String + /** + * Converter class - class that should be instantiated to construct the converter. + * + * Technically could be a function, but this would be playing with fire. + */ + public lateinit var converterClass: String - /** Argument type, the type the user should ultimately be given after parsing. **/ - public lateinit var argumentType: String - - /** Builder generic params, if any. Omit the `<>`. **/ - public var builderGeneric: String? = null + /** Argument type, the type the user should ultimately be given after parsing. **/ + public lateinit var argumentType: String - /** Extra generic bounds to place after `where` in the builder signature. **/ - public var whereSuffix: String? = null + /** Builder generic params, if any. Omit the `<>`. **/ + public var builderGeneric: String? = null - internal val builderArguments: MutableList = mutableListOf() - internal val builderArgumentNames: MutableList = mutableListOf() - - internal val builderFields: MutableList = mutableListOf() - internal val builderFieldNames: MutableList = mutableListOf() - - internal val builderBuildFunctionPreStatements: MutableList = mutableListOf() - internal val builderBuildFunctionStatements: MutableList = mutableListOf() - internal val builderExtraStatements: MutableList = mutableListOf() - internal val builderInitStatements: MutableList = mutableListOf() + /** Extra generic bounds to place after `where` in the builder signature. **/ + public var whereSuffix: String? = null - internal val types: MutableSet = mutableSetOf() + internal val builderArguments: MutableList = mutableListOf() + internal val builderArgumentNames: MutableList = mutableListOf() - internal var converterType: String = "" - internal var functionSuffix: String = "" - internal var builderType: String = "" + internal val builderFields: MutableList = mutableListOf() + internal val builderFieldNames: MutableList = mutableListOf() - /** The ultimate result, the final string created after calling [build]. **/ - public var result: String? = null - private set + internal val builderBuildFunctionPreStatements: MutableList = mutableListOf() + internal val builderBuildFunctionStatements: MutableList = mutableListOf() + internal val builderExtraStatements: MutableList = mutableListOf() + internal val builderInitStatements: MutableList = mutableListOf() - /** Add a builder constructor argument. **/ - public fun builderArg(arg: String) { - builderArguments.add(arg) + internal val types: MutableSet = mutableSetOf() - if (!arg.startsWith("!!")) { - builderArgumentNames.add(arg.split(":").first().split(" ").last()) - } - } + internal var converterType: String = "" + internal var functionSuffix: String = "" + internal var builderType: String = "" - /** Add a builder field. **/ - public fun builderField(field: String) { - builderFields.add(field) - builderFieldNames.add(field.split(":").first().split(" ").last()) - } + /** The ultimate result, the final string created after calling [build]. **/ + public var result: String? = null + private set - /** Add a builder build function statement. **/ - public fun builderBuildFunctionPreStatement(line: String) { - builderBuildFunctionPreStatements.add(line) - } + /** Add a builder constructor argument. **/ + public fun builderArg(arg: String) { + builderArguments.add(arg) - /** Add a builder build function statement. **/ - public fun builderBuildFunctionStatement(line: String) { - builderBuildFunctionStatements.add(line) - } + if (!arg.startsWith("!!")) { + builderArgumentNames.add(arg.split(":").first().split(" ").last()) + } + } + + /** Add a builder field. **/ + public fun builderField(field: String) { + builderFields.add(field) + builderFieldNames.add(field.split(":").first().split(" ").last()) + } + + /** Add a builder build function statement. **/ + public fun builderBuildFunctionPreStatement(line: String) { + builderBuildFunctionPreStatements.add(line) + } + + /** Add a builder build function statement. **/ + public fun builderBuildFunctionStatement(line: String) { + builderBuildFunctionStatements.add(line) + } + + /** Add a builder extra statement. **/ + public fun builderExtraStatement(line: String) { + builderExtraStatements.add(line) + } + + /** Add a builder init statement. **/ + public fun builderInitStatement(line: String) { + builderInitStatements.add(line) + } + + /** Specify the converter types that this builder concerns. **/ + public fun types(vararg types: ConverterType) { + types(types.toList()) + } + + /** Specify the converter types that this builder concerns. **/ + public fun types(types: Collection) { + this.types.addAll(types) + } + + /** Build the string that contains this builder's code. It's also stored in [result]. **/ + public fun build(): String { + builderType = "" + + val builder = StringBuilder() + + if (comment != null) { + builder.append("/**\n") + + comment!!.lines().forEach { + builder.append(" * $it\n") + } + + builder.append(" */\n") + } + + builder.append("public class ") + + types.groupBy { it.order }.forEach { (_, entries) -> + if (entries.size > 1) { + error("Only one of the following converter types may be specified at once: ${entries.joinToString()}") + } + } + + converterType = buildString { + types.sortedBy { it.order }.forEach { + if (it.appendFragment) { + append(it.fragment) + } + } + + builderType = this.toString() + + name + + + if (ConverterType.CHOICE in types) { + "Choice" + } else { + "" + } + + + "ConverterBuilder" + + builder.append(builderType) - /** Add a builder extra statement. **/ - public fun builderExtraStatement(line: String) { - builderExtraStatements.add(line) - } + if (builderGeneric != null) { + builder.append(" <$builderGeneric>") + } + + builder.append("( /** @inject: builderConstructorArguments **/ ") + + if (builderArguments.isNotEmpty()) { + builder.append("\n") + + builderArguments.forEach { + builder.append(" ${it.trim('!', ' ')},\n") + } + } + + builder.append(") : ${this}ConverterBuilder<$argumentType>()") + + if (ConverterType.SINGLE in types && this.isEmpty()) { + append("Single") + } + + append("Converter") + } + + if (ConverterType.CHOICE in types) { + builder.append(", ChoiceConverterBuilder<$argumentType>") + } - /** Add a builder init statement. **/ - public fun builderInitStatement(line: String) { - builderInitStatements.add(line) - } + if (whereSuffix != null) { + builder.append(" where $whereSuffix") + } - /** Specify the converter types that this builder concerns. **/ - public fun types(vararg types: ConverterType) { - types(types.toList()) - } + builder.append(" {\n") - /** Specify the converter types that this builder concerns. **/ - public fun types(types: Collection) { - this.types.addAll(types) - } + if (ConverterType.CHOICE in types) { + builder.append(" override var choices: MutableMap = mutableMapOf()\n\n") + } - /** Build the string that contains this builder's code. It's also stored in [result]. **/ - public fun build(): String { - builderType = "" + builder.append(" /** @inject: builderFields **/\n") - val builder = StringBuilder() + if (builderFields.isNotEmpty()) { + builderFields.forEach { + builder.append(" $it\n") + } + } - if (comment != null) { - builder.append("/**\n") + builder.append("\n") - comment!!.lines().forEach { - builder.append(" * $it\n") - } + builder.append(" init {\n") + builder.append(" /** @inject: builderInitStatements **/\n") - builder.append(" */\n") - } + if (builderInitStatements.isNotEmpty()) { + builderInitStatements.forEach { + builder.append(" $it\n") + } + } - builder.append("public class ") + builder.append(" }\n\n") + builder.append(" /** @inject: builderExtraStatements **/\n") - types.groupBy { it.order }.forEach { (_, entries) -> - if (entries.size > 1) { - error("Only one of the following converter types may be specified at once: ${entries.joinToString()}") - } - } + if (builderExtraStatements.isNotEmpty()) { + builderExtraStatements.forEach { + builder.append(" $it\n") + } + } - converterType = buildString { - types.sortedBy { it.order }.forEach { - if (it.appendFragment) { - append(it.fragment) - } - } + builder.append("\n") - builderType = this.toString() + - name + + builder.append(" public override fun build(arguments: Arguments): $converterType<$argumentType> {\n") + builder.append(" /** @inject: builderBuildFunctionPreStatements **/\n\n") - if (ConverterType.CHOICE in types) { - "Choice" - } else { - "" - } + + if (builderBuildFunctionPreStatements.isNotEmpty()) { + builderBuildFunctionPreStatements.forEach { + builder.append(" $it\n") + } + } - "ConverterBuilder" + builder.append(" val converter = $converterClass(\n") - builder.append(builderType) + if (!types.containsAny(ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.DEFAULTING)) { + builder.append(" validator = validator,\n") + } - if (builderGeneric != null) { - builder.append(" <$builderGeneric>") - } + if (ConverterType.COALESCING in types) { + builder.append(" shouldThrow = !ignoreErrors,\n") + } - builder.append("( /** @inject: builderConstructorArguments **/ ") + if (ConverterType.CHOICE in types) { + builder.append(" choices = choices,\n") + } - if (builderArguments.isNotEmpty()) { - builder.append("\n") + builderArgumentNames.forEach { + builder.append(" $it = $it,\n") + } - builderArguments.forEach { - builder.append(" ${it.trim('!', ' ')},\n") - } - } + builderFieldNames.forEach { + builder.append(" $it = $it,\n") + } - builder.append(") : ${this}ConverterBuilder<$argumentType>()") + builder.append(" )\n\n") + builder.append(" /** @inject: builderBuildFunctionStatements **/\n") - if (ConverterType.SINGLE in types && this.isEmpty()) { - append("Single") - } + if (builderBuildFunctionStatements.isNotEmpty()) { + builderBuildFunctionStatements.forEach { + builder.append(" $it\n") + } + } - append("Converter") - } + builder.append("\n") + + builder.append(" return arguments.arg(\n") + builder.append(" displayName = name,\n") + builder.append(" description = description,\n") + builder.append("\n") + builder.append(" converter = converter") + + if (types.contains(ConverterType.DEFAULTING)) { + builder.append(".toDefaulting(\n") + builder.append(" defaultValue = defaultValue,\n") + builder.append(" outputError = !ignoreErrors,\n") + builder.append(" nestedValidator = validator,\n") + builder.append(" )") + } else if (types.contains(ConverterType.OPTIONAL)) { + builder.append(".toOptional(\n") + builder.append(" outputError = !ignoreErrors,\n") + builder.append(" nestedValidator = validator,\n") + builder.append(" )") + } else if (types.contains(ConverterType.LIST)) { + builder.append(".toList(\n") + builder.append(" required = !ignoreErrors,\n") + builder.append(" nestedValidator = validator,\n") + builder.append(" )") + } - if (ConverterType.CHOICE in types) { - builder.append(", ChoiceConverterBuilder<$argumentType>") - } + builder.append(".withBuilder(this)") - if (whereSuffix != null) { - builder.append(" where $whereSuffix") - } + builder.append("\n") + builder.append(" )\n") + builder.append(" }\n") - builder.append(" {\n") + val lateInit = builderFields + .filter { it.contains("lateinit var") } + .map { it.split(":").first() } + .map { it.split(" ").last() } - if (ConverterType.CHOICE in types) { - builder.append(" override var choices: MutableMap = mutableMapOf()\n\n") - } + if (lateInit.isNotEmpty()) { + builder.append("\n") - builder.append(" /** @inject: builderFields **/\n") + builder.append(" override fun validateArgument() {\n") + builder.append(" super.validateArgument()\n") - if (builderFields.isNotEmpty()) { - builderFields.forEach { - builder.append(" $it\n") - } - } + lateInit.forEach { + builder.append("\n") + builder.append(" if (!this::$it.isInitialized) {\n") - builder.append("\n") + builder.append( + " throw InvalidArgumentException(this, " + + "\"Required field not provided: $it\")\n" + ) - builder.append(" init {\n") - builder.append(" /** @inject: builderInitStatements **/\n") + builder.append(" }\n") + } - if (builderInitStatements.isNotEmpty()) { - builderInitStatements.forEach { - builder.append(" $it\n") - } - } + builder.append(" }\n") + } - builder.append(" }\n\n") - builder.append(" /** @inject: builderExtraStatements **/\n") + builder.append("}\n") - if (builderExtraStatements.isNotEmpty()) { - builderExtraStatements.forEach { - builder.append(" $it\n") - } - } + result = builder.toString() - builder.append("\n") + functionSuffix = converterType.removeSuffix("Converter") - builder.append(" public override fun build(arguments: Arguments): $converterType<$argumentType> {\n") - builder.append(" /** @inject: builderBuildFunctionPreStatements **/\n\n") + return result!! + } - if (builderBuildFunctionPreStatements.isNotEmpty()) { - builderBuildFunctionPreStatements.forEach { - builder.append(" $it\n") - } - } + internal fun getFunctionName(givenName: String): String { + val before: MutableList = mutableListOf() + val after: MutableList = mutableListOf() - builder.append(" val converter = $converterClass(\n") + for (type in types) { + when (type) { + ConverterType.DEFAULTING -> before.add("defaulting") + ConverterType.LIST -> after.add("list") + ConverterType.OPTIONAL -> before.add("optional") + ConverterType.COALESCING -> before.add("coalescing") + ConverterType.CHOICE -> after.add("choice") - if (!types.containsAny(ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.DEFAULTING)) { - builder.append(" validator = validator,\n") - } + ConverterType.SINGLE -> { + /* Don't add anything */ + } + } + } - if (ConverterType.COALESCING in types) { - builder.append(" shouldThrow = !ignoreErrors,\n") + val capitalizeName = before.isNotEmpty() + var firstString = true + + val resultString = buildString { + before.forEach { + if (firstString) { + append(it) + firstString = false + } else { + append(it.toCapitalized()) + } + } + + if (capitalizeName) { + append(givenName.toCapitalized()) + } else { + append(givenName) + } + + after.forEach { + append(it.toCapitalized()) + } } - if (ConverterType.CHOICE in types) { - builder.append(" choices = choices,\n") - } - - builderArgumentNames.forEach { - builder.append(" $it = $it,\n") - } - - builderFieldNames.forEach { - builder.append(" $it = $it,\n") - } - - builder.append(" )\n\n") - builder.append(" /** @inject: builderBuildFunctionStatements **/\n") - - if (builderBuildFunctionStatements.isNotEmpty()) { - builderBuildFunctionStatements.forEach { - builder.append(" $it\n") - } - } - - builder.append("\n") - - builder.append(" return arguments.arg(\n") - builder.append(" displayName = name,\n") - builder.append(" description = description,\n") - builder.append("\n") - builder.append(" converter = converter") - - if (types.contains(ConverterType.DEFAULTING)) { - builder.append(".toDefaulting(\n") - builder.append(" defaultValue = defaultValue,\n") - builder.append(" outputError = !ignoreErrors,\n") - builder.append(" nestedValidator = validator,\n") - builder.append(" )") - } else if (types.contains(ConverterType.OPTIONAL)) { - builder.append(".toOptional(\n") - builder.append(" outputError = !ignoreErrors,\n") - builder.append(" nestedValidator = validator,\n") - builder.append(" )") - } else if (types.contains(ConverterType.LIST)) { - builder.append(".toList(\n") - builder.append(" required = !ignoreErrors,\n") - builder.append(" nestedValidator = validator,\n") - builder.append(" )") - } - - builder.append(".withBuilder(this)") - - builder.append("\n") - builder.append(" )\n") - builder.append(" }\n") - - val lateInit = builderFields - .filter { it.contains("lateinit var") } - .map { it.split(":").first() } - .map { it.split(" ").last() } - - if (lateInit.isNotEmpty()) { - builder.append("\n") - - builder.append(" override fun validateArgument() {\n") - builder.append(" super.validateArgument()\n") - - lateInit.forEach { - builder.append("\n") - builder.append(" if (!this::$it.isInitialized) {\n") - - builder.append( - " throw InvalidArgumentException(this, " + - "\"Required field not provided: $it\")\n" - ) - - builder.append(" }\n") - } - - builder.append(" }\n") - } - - builder.append("}\n") - - result = builder.toString() - - functionSuffix = converterType.removeSuffix("Converter") - - return result!! - } - - internal fun getFunctionName(givenName: String): String { - val before: MutableList = mutableListOf() - val after: MutableList = mutableListOf() - - for (type in types) { - when (type) { - ConverterType.DEFAULTING -> before.add("defaulting") - ConverterType.LIST -> after.add("list") - ConverterType.OPTIONAL -> before.add("optional") - ConverterType.COALESCING -> before.add("coalescing") - ConverterType.CHOICE -> after.add("choice") - - ConverterType.SINGLE -> { - /* Don't add anything */ - } - } - } - - val capitalizeName = before.isNotEmpty() - var firstString = true - - val resultString = buildString { - before.forEach { - if (firstString) { - append(it) - firstString = false - } else { - append(it.toCapitalized()) - } - } - - if (capitalizeName) { - append(givenName.toCapitalized()) - } else { - append(givenName) - } - - after.forEach { - append(it.toCapitalized()) - } - } - - return resultString - } + return resultString + } } /** DSL function to easily build a converter builder class. Returns the builder. **/ public fun builderClass(body: ConverterBuilderClassBuilder.() -> Unit): ConverterBuilderClassBuilder { - val builder = ConverterBuilderClassBuilder() + val builder = ConverterBuilderClassBuilder() - body(builder) + body(builder) - builder.build() + builder.build() - return builder + return builder } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/builders/ConverterBuilderFunctionBuilder.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/builders/ConverterBuilderFunctionBuilder.kt index 3514f19755..ff5c98f3c2 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/builders/ConverterBuilderFunctionBuilder.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/builders/ConverterBuilderFunctionBuilder.kt @@ -5,7 +5,7 @@ */ @file:Suppress( - "StringLiteralDuplication" + "StringLiteralDuplication" ) package com.kotlindiscord.kord.extensions.modules.annotations.converters.builders @@ -18,123 +18,123 @@ import org.koin.core.component.KoinComponent * This is a fairly messy class containing a string builder, but what can you do? */ public class ConverterBuilderFunctionBuilder : KoinComponent { - /** Comment to prepend to the function definition. **/ - public var comment: String? = null + /** Comment to prepend to the function definition. **/ + public var comment: String? = null - /** Builder class generic arguments. Omit the `<>`. **/ - public var builderGeneric: String? = null + /** Builder class generic arguments. Omit the `<>`. **/ + public var builderGeneric: String? = null - /** Builder function generic arguments. Omit the `<>`. **/ - public var functionGeneric: String? = null + /** Builder function generic arguments. Omit the `<>`. **/ + public var functionGeneric: String? = null - /** Extra generic bounds to place after `where` in the function signature. **/ - public var whereSuffix: String? = null + /** Extra generic bounds to place after `where` in the function signature. **/ + public var whereSuffix: String? = null - internal val builderArguments: MutableList = mutableListOf() + internal val builderArguments: MutableList = mutableListOf() - /** Argument function name, `"Arguments.$name"`. **/ - public lateinit var name: String + /** Argument function name, `"Arguments.$name"`. **/ + public lateinit var name: String - /** Builder class type, used as a receiver. **/ - public lateinit var builderType: String + /** Builder class type, used as a receiver. **/ + public lateinit var builderType: String - /** Argument type, the type the user should ultimately be given after parsing. **/ - public lateinit var argumentType: String + /** Argument type, the type the user should ultimately be given after parsing. **/ + public lateinit var argumentType: String - /** Basic converter type, returned by the function. **/ - public lateinit var converterType: String + /** Basic converter type, returned by the function. **/ + public lateinit var converterType: String - /** - * Add a builder constructor argument. `name = value` only, please. - * - * If this is a lambda, you can refer to the outer `builder` to get values provided by the user. - */ - public fun builderArg(arg: String): Boolean = - builderArguments.add(arg) + /** + * Add a builder constructor argument. `name = value` only, please. + * + * If this is a lambda, you can refer to the outer `builder` to get values provided by the user. + */ + public fun builderArg(arg: String): Boolean = + builderArguments.add(arg) - /** Build the string that contains this builder's code. **/ - public fun build(): String { - val builder = StringBuilder() + /** Build the string that contains this builder's code. **/ + public fun build(): String { + val builder = StringBuilder() - if (comment != null) { - builder.append("/**\n") + if (comment != null) { + builder.append("/**\n") - comment!!.lines().forEach { - builder.append(" * $it\n") - } + comment!!.lines().forEach { + builder.append(" * $it\n") + } - builder.append(" */\n") - } + builder.append(" */\n") + } - builder.append("public ") + builder.append("public ") - if (functionGeneric != null) { - builder.append("inline ") - } + if (functionGeneric != null) { + builder.append("inline ") + } - builder.append("fun ") + builder.append("fun ") - if (functionGeneric != null) { - builder.append(" ") - } + if (functionGeneric != null) { + builder.append(" ") + } - builder.append("Arguments.$name(\n") + builder.append("Arguments.$name(\n") - builder.append(" body: $builderType") + builder.append(" body: $builderType") - val splitBuilderGeneric = builderGeneric?.split(",") - ?.joinToString { it.split(":").first() } + val splitBuilderGeneric = builderGeneric?.split(",") + ?.joinToString { it.split(":").first() } - if (splitBuilderGeneric != null) { - builder.append("<$splitBuilderGeneric>") - } + if (splitBuilderGeneric != null) { + builder.append("<$splitBuilderGeneric>") + } - builder.append(".() -> Unit\n") + builder.append(".() -> Unit\n") - builder.append("): $converterType<$argumentType>") + builder.append("): $converterType<$argumentType>") - if (whereSuffix != null) { - builder.append(" where $whereSuffix") - } + if (whereSuffix != null) { + builder.append(" where $whereSuffix") + } - builder.append(" {\n") - builder.append(" val builder = $builderType") + builder.append(" {\n") + builder.append(" val builder = $builderType") - if (splitBuilderGeneric != null) { - builder.append("<$splitBuilderGeneric>") - } + if (splitBuilderGeneric != null) { + builder.append("<$splitBuilderGeneric>") + } - builder.append("( /** @inject: functionBuilderArguments **/ ") + builder.append("( /** @inject: functionBuilderArguments **/ ") - if (builderArguments.isNotEmpty()) { - builder.append("\n") + if (builderArguments.isNotEmpty()) { + builder.append("\n") - builderArguments.forEach { - builder.append(" $it,\n") - } + builderArguments.forEach { + builder.append(" $it,\n") + } - builder.append(" ") - } + builder.append(" ") + } - builder.append(")\n") + builder.append(")\n") - builder.append(" \n") - builder.append(" body(builder)\n") - builder.append(" \n") - builder.append(" builder.validateArgument()\n") - builder.append(" \n") - builder.append(" return builder.build(this)\n") - builder.append("}") + builder.append(" \n") + builder.append(" body(builder)\n") + builder.append(" \n") + builder.append(" builder.validateArgument()\n") + builder.append(" \n") + builder.append(" return builder.build(this)\n") + builder.append("}") - return builder.toString() - } + return builder.toString() + } } /** DSL function to easily build a converter builder class. Returns a String. **/ public fun builderFunction(body: ConverterBuilderFunctionBuilder.() -> Unit): String { - val builder = ConverterBuilderFunctionBuilder() + val builder = ConverterBuilderFunctionBuilder() - body(builder) + body(builder) - return builder.build() + return builder.build() } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/plugins/PluginArgs.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/plugins/PluginArgs.kt index 00775039c1..34d4619937 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/plugins/PluginArgs.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/plugins/PluginArgs.kt @@ -15,38 +15,38 @@ import com.kotlindiscord.kord.extensions.modules.annotations.orNull * @property annotation Annotation definition to extract data from. */ public data class PluginArgs(public val annotation: KSAnnotation) { - /** @suppress **/ - private val argMap: Map = - annotation.arguments - .associate { it.name?.getShortName() to it.value } - .filterKeys { it != null } - - /** @suppress **/ - public val id: String = - argMap["id"] as String - - /** @suppress **/ - public val version: String = - argMap["version"] as String - - /** @suppress **/ - public val author: String? = - (argMap["author"] as String).orNull() - - /** @suppress **/ - public val description: String? = - (argMap["description"] as String).orNull() - - /** @suppress **/ - public val license: String? = - (argMap["license"] as String).orNull() - - /** @suppress **/ - public val kordExVersion: String? = - (argMap["kordExVersion"] as String).orNull() - - /** @suppress **/ - @Suppress("UNCHECKED_CAST") - public val dependencies: ArrayList = - argMap["dependencies"] as ArrayList? ?: arrayListOf() + /** @suppress **/ + private val argMap: Map = + annotation.arguments + .associate { it.name?.getShortName() to it.value } + .filterKeys { it != null } + + /** @suppress **/ + public val id: String = + argMap["id"] as String + + /** @suppress **/ + public val version: String = + argMap["version"] as String + + /** @suppress **/ + public val author: String? = + (argMap["author"] as String).orNull() + + /** @suppress **/ + public val description: String? = + (argMap["description"] as String).orNull() + + /** @suppress **/ + public val license: String? = + (argMap["license"] as String).orNull() + + /** @suppress **/ + public val kordExVersion: String? = + (argMap["kordExVersion"] as String).orNull() + + /** @suppress **/ + @Suppress("UNCHECKED_CAST") + public val dependencies: ArrayList = + argMap["dependencies"] as ArrayList? ?: arrayListOf() } diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/plugins/PluginProcessor.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/plugins/PluginProcessor.kt index 7b624e2a2c..2f9de5cbe8 100644 --- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/plugins/PluginProcessor.kt +++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/plugins/PluginProcessor.kt @@ -15,102 +15,102 @@ import java.util.* /** Annotation processor for processing wired plugin "plugins". **/ public class PluginProcessor( - private val generator: CodeGenerator, - private val logger: KSPLogger + private val generator: CodeGenerator, + private val logger: KSPLogger, ) : SymbolProcessor { - override fun process(resolver: Resolver): List { - val plugins = resolver.getSymbolsWithAnnotation( - "com.kotlindiscord.kord.extensions.plugins.annotations.plugins.WiredPlugin" - ) - - val ret = plugins - .filter { !it.validate() } - .toList() - - ret.forEach { - logger.warn("Unable to validate: $it") - } - - val toProcess = plugins - .filterIsInstance() - .filter { symbol -> - symbol.validate() && - symbol.superTypes - .mapNotNull { it.resolve().declaration as? KSClassDeclaration } - .mapNotNull { it.qualifiedName?.asString() } - .contains("com.kotlindiscord.kord.extensions.plugins.KordExPlugin") - }.toList() - - val wrongSuperType = plugins - .filterIsInstance() - .filter { symbol -> - !( - symbol.validate() && - symbol.superTypes - .mapNotNull { it.resolve().declaration as? KSClassDeclaration } - .mapNotNull { it.qualifiedName?.asString() } - .contains("com.kotlindiscord.kord.extensions.plugins.KordExPlugin") - ) - }.toList() - - wrongSuperType.forEach { - logger.error( - "Annotated class does not extend KordExPlugin: ${it.qualifiedName?.asString()}" - ) - } - - if (toProcess.size > 1) { - logger.error( - "This project contains more than one wired plugin. Only the first one will be processed." - ) - } - - toProcess.firstOrNull()?.accept(PluginVisitor(), Unit) - - return ret - } - - /** Visitor for plugin classes. **/ - public inner class PluginVisitor : KSVisitorVoid() { - override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { - val annotation = classDeclaration.annotations.filter { - it.annotationType.resolve().declaration.qualifiedName?.asString() == - "com.kotlindiscord.kord.extensions.plugins.annotations.plugins.WiredPlugin" - }.first() - - val file = generator.createNewFile( - Dependencies(true, classDeclaration.containingFile!!), - "META-INF", "plugin", "properties" - ) - - val props = Properties() - val args = PluginArgs(annotation) - - props["plugin.class"] = classDeclaration.qualifiedName!!.asString() - props["plugin.id"] = args.id - props["plugin.version"] = args.version - - if (args.dependencies.isNotEmpty()) { - props["plugin.dependencies"] = args.dependencies.joinToString() - } - - args.kordExVersion?.let { - props["plugin.requires"] = it - } - - args.description?.let { - props["plugin.description"] = it - } - - args.author?.let { - props["plugin.provider"] = it - } - - args.license?.let { - props["plugin.license"] = it - } - - props.store(file.writer(), "Generated plugin/\"plugin\" definition.") - } - } + override fun process(resolver: Resolver): List { + val plugins = resolver.getSymbolsWithAnnotation( + "com.kotlindiscord.kord.extensions.plugins.annotations.plugins.WiredPlugin" + ) + + val ret = plugins + .filter { !it.validate() } + .toList() + + ret.forEach { + logger.warn("Unable to validate: $it") + } + + val toProcess = plugins + .filterIsInstance() + .filter { symbol -> + symbol.validate() && + symbol.superTypes + .mapNotNull { it.resolve().declaration as? KSClassDeclaration } + .mapNotNull { it.qualifiedName?.asString() } + .contains("com.kotlindiscord.kord.extensions.plugins.KordExPlugin") + }.toList() + + val wrongSuperType = plugins + .filterIsInstance() + .filter { symbol -> + !( + symbol.validate() && + symbol.superTypes + .mapNotNull { it.resolve().declaration as? KSClassDeclaration } + .mapNotNull { it.qualifiedName?.asString() } + .contains("com.kotlindiscord.kord.extensions.plugins.KordExPlugin") + ) + }.toList() + + wrongSuperType.forEach { + logger.error( + "Annotated class does not extend KordExPlugin: ${it.qualifiedName?.asString()}" + ) + } + + if (toProcess.size > 1) { + logger.error( + "This project contains more than one wired plugin. Only the first one will be processed." + ) + } + + toProcess.firstOrNull()?.accept(PluginVisitor(), Unit) + + return ret + } + + /** Visitor for plugin classes. **/ + public inner class PluginVisitor : KSVisitorVoid() { + override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { + val annotation = classDeclaration.annotations.filter { + it.annotationType.resolve().declaration.qualifiedName?.asString() == + "com.kotlindiscord.kord.extensions.plugins.annotations.plugins.WiredPlugin" + }.first() + + val file = generator.createNewFile( + Dependencies(true, classDeclaration.containingFile!!), + "META-INF", "plugin", "properties" + ) + + val props = Properties() + val args = PluginArgs(annotation) + + props["plugin.class"] = classDeclaration.qualifiedName!!.asString() + props["plugin.id"] = args.id + props["plugin.version"] = args.version + + if (args.dependencies.isNotEmpty()) { + props["plugin.dependencies"] = args.dependencies.joinToString() + } + + args.kordExVersion?.let { + props["plugin.requires"] = it + } + + args.description?.let { + props["plugin.description"] = it + } + + args.author?.let { + props["plugin.provider"] = it + } + + args.license?.let { + props["plugin.license"] = it + } + + props.store(file.writer(), "Generated plugin/\"plugin\" definition.") + } + } } diff --git a/annotations/build.gradle.kts b/annotations/build.gradle.kts index 64f3927869..f9a95dd4ac 100644 --- a/annotations/build.gradle.kts +++ b/annotations/build.gradle.kts @@ -1,20 +1,20 @@ plugins { - `kordex-module` - `published-module` + `kordex-module` + `published-module` } metadata { - name = "KordEx: Annotations" - description = "Annotation definitions to be processed by the KordEx annotation processor" + name = "KordEx: Annotations" + description = "Annotation definitions to be processed by the KordEx annotation processor" } dependencies { - implementation(libs.kotlin.stdlib) + implementation(libs.kotlin.stdlib) - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) } dokkaModule { - moduleName.set("Kord Extensions: Annotations") + moduleName.set("Kord Extensions: Annotations") } diff --git a/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/Converter.kt b/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/Converter.kt index c61004e1a8..6d9885bdd2 100644 --- a/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/Converter.kt +++ b/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/Converter.kt @@ -35,22 +35,22 @@ package com.kotlindiscord.kord.extensions.modules.annotations.converters @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.CLASS) public annotation class Converter( - public vararg val names: String, + public vararg val names: String, - public val types: Array, - public val imports: Array = [], + public val types: Array, + public val imports: Array = [], - public val builderConstructorArguments: Array = [], - public val builderGeneric: String = "", - public val builderFields: Array = [], - public val builderSuffixedWhere: String = "", + public val builderConstructorArguments: Array = [], + public val builderGeneric: String = "", + public val builderFields: Array = [], + public val builderSuffixedWhere: String = "", - public val builderBuildFunctionPreStatements: Array = [], - public val builderBuildFunctionStatements: Array = [], - public val builderInitStatements: Array = [], - public val builderExtraStatements: Array = [], + public val builderBuildFunctionPreStatements: Array = [], + public val builderBuildFunctionStatements: Array = [], + public val builderInitStatements: Array = [], + public val builderExtraStatements: Array = [], - public val functionBuilderArguments: Array = [], - public val functionGeneric: String = "", - public val functionSuffixedWhere: String = "", + public val functionBuilderArguments: Array = [], + public val functionGeneric: String = "", + public val functionSuffixedWhere: String = "", ) diff --git a/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterType.kt b/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterType.kt index 11918bf88b..60a0d86406 100644 --- a/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterType.kt +++ b/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterType.kt @@ -14,33 +14,33 @@ package com.kotlindiscord.kord.extensions.modules.annotations.converters * @property appendFragment Whether to append the string fragment, or ignore it */ public enum class ConverterType( - public val fragment: String, - public val order: Int, - public val appendFragment: Boolean = true + public val fragment: String, + public val order: Int, + public val appendFragment: Boolean = true, ) { - DEFAULTING("Defaulting", 0), - LIST("List", 0), - OPTIONAL("Optional", 0), + DEFAULTING("Defaulting", 0), + LIST("List", 0), + OPTIONAL("Optional", 0), - COALESCING("Coalescing", 1), - SINGLE("", 1), + COALESCING("Coalescing", 1), + SINGLE("", 1), - CHOICE("Choice", 2, false); + CHOICE("Choice", 2, false); - public companion object { - /** Given the `.name` or `.simpleName` of a converter type, get the relevant enum entry. **/ - public fun fromName(name: String): ConverterType? = - when (name) { - DEFAULTING.name -> DEFAULTING - LIST.name -> LIST - OPTIONAL.name -> OPTIONAL + public companion object { + /** Given the `.name` or `.simpleName` of a converter type, get the relevant enum entry. **/ + public fun fromName(name: String): ConverterType? = + when (name) { + DEFAULTING.name -> DEFAULTING + LIST.name -> LIST + OPTIONAL.name -> OPTIONAL - COALESCING.name -> COALESCING - SINGLE.name -> SINGLE + COALESCING.name -> COALESCING + SINGLE.name -> SINGLE - CHOICE.name -> CHOICE + CHOICE.name -> CHOICE - else -> null - } - } + else -> null + } + } } diff --git a/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/annotations/plugins/WiredPlugin.kt b/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/annotations/plugins/WiredPlugin.kt index 777a0ef2fc..2f9ef84824 100644 --- a/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/annotations/plugins/WiredPlugin.kt +++ b/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/annotations/plugins/WiredPlugin.kt @@ -26,13 +26,13 @@ package com.kotlindiscord.kord.extensions.plugins.annotations.plugins @Target(AnnotationTarget.CLASS) @MustBeDocumented public annotation class WiredPlugin( - public val id: String, - public val version: String, + public val id: String, + public val version: String, - public val author: String = "", - public val description: String = "", - public val license: String = "", - public val kordExVersion: String = "", + public val author: String = "", + public val description: String = "", + public val license: String = "", + public val kordExVersion: String = "", - public val dependencies: Array = [], + public val dependencies: Array = [], ) diff --git a/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/tooling/TranslatableType.kt b/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/tooling/TranslatableType.kt index 5e4736910e..274d03a319 100644 --- a/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/tooling/TranslatableType.kt +++ b/annotations/src/main/kotlin/com/kotlindiscord/kord/extensions/tooling/TranslatableType.kt @@ -4,7 +4,7 @@ package com.kotlindiscord.kord.extensions.tooling * Enum explaining how an annotated type relates to the translation system. */ public enum class TranslatableType { - /** The annotated element specifies a bundle name. **/ + /** The annotated element specifies a bundle name. **/ BUNDLE, /** The annotated element specifies a locale. **/ diff --git a/build.gradle.kts b/build.gradle.kts index 88dd9cb26e..0c066faa41 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,21 +1,21 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { - repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } - } + repositories { + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } } plugins { - `maven-publish` + `maven-publish` - kotlin("jvm") + kotlin("jvm") - id("com.github.jakemarsden.git-hooks") - id("org.jetbrains.dokka") + id("com.github.jakemarsden.git-hooks") + id("org.jetbrains.dokka") } val projectVersion: String by project @@ -24,42 +24,42 @@ group = "com.kotlindiscord.kord.extensions" version = projectVersion val printVersion = task("printVersion") { - doLast { - print(version.toString()) - } + doLast { + print(version.toString()) + } } gitHooks { - setHooks(mapOf("pre-commit" to "updateLicenses detekt")) + setHooks(mapOf("pre-commit" to "updateLicenses detekt")) } repositories { - google() - mavenCentral() + google() + mavenCentral() - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } } subprojects { - group = "com.kotlindiscord.kord.extensions" - version = projectVersion + group = "com.kotlindiscord.kord.extensions" + version = projectVersion - tasks.withType { - kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" - kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.contracts.ExperimentalContracts" - } + tasks.withType { + kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.contracts.ExperimentalContracts" + } - repositories { - rootProject.repositories.forEach { - if (it is MavenArtifactRepository) { - maven { - name = it.name - url = it.url - } - } - } - } + repositories { + rootProject.repositories.forEach { + if (it is MavenArtifactRepository) { + maven { + name = it.name + url = it.url + } + } + } + } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 7e7c64d377..a3282437c1 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,23 +1,23 @@ plugins { - `kotlin-dsl` + `kotlin-dsl` } repositories { - google() - gradlePluginPortal() + google() + gradlePluginPortal() } dependencies { - implementation(kotlin("gradle-plugin", version = "1.9.22")) - implementation(kotlin("serialization", version = "1.9.22")) + implementation(kotlin("gradle-plugin", version = "1.9.22")) + implementation(kotlin("serialization", version = "1.9.22")) - implementation("gradle.plugin.org.cadixdev.gradle", "licenser", "0.6.1") + implementation("gradle.plugin.org.cadixdev.gradle", "licenser", "0.6.1") implementation("com.github.ben-manes", "gradle-versions-plugin", "0.50.0") - implementation("com.github.jakemarsden", "git-hooks-gradle-plugin", "0.0.2") - implementation("com.google.devtools.ksp", "com.google.devtools.ksp.gradle.plugin", "1.9.22-1.0.17") - implementation("io.gitlab.arturbosch.detekt", "detekt-gradle-plugin", "1.23.5") - implementation("org.jetbrains.dokka", "dokka-gradle-plugin", "1.9.10") + implementation("com.github.jakemarsden", "git-hooks-gradle-plugin", "0.0.2") + implementation("com.google.devtools.ksp", "com.google.devtools.ksp.gradle.plugin", "1.9.22-1.0.17") + implementation("io.gitlab.arturbosch.detekt", "detekt-gradle-plugin", "1.23.5") + implementation("org.jetbrains.dokka", "dokka-gradle-plugin", "1.9.10") - implementation(gradleApi()) - implementation(localGroovy()) + implementation(gradleApi()) + implementation(localGroovy()) } diff --git a/buildSrc/src/main/kotlin/GitTools.kt b/buildSrc/src/main/kotlin/GitTools.kt index cf59b32922..658e08c63d 100644 --- a/buildSrc/src/main/kotlin/GitTools.kt +++ b/buildSrc/src/main/kotlin/GitTools.kt @@ -2,24 +2,24 @@ import org.gradle.api.Project import java.io.ByteArrayOutputStream fun Project.runCommand(command: String): String { - val output = ByteArrayOutputStream() + val output = ByteArrayOutputStream() - exec { - commandLine(command.split(" ")) - standardOutput = output - } + exec { + commandLine(command.split(" ")) + standardOutput = output + } - return output.toString().trim() + return output.toString().trim() } fun Project.getCurrentGitBranch(): String { // https://gist.github.com/lordcodes/15b2a4aecbeff7c3238a70bfd20f0931 - var gitBranch = "Unknown branch" + var gitBranch = "Unknown branch" - try { - gitBranch = runCommand("git rev-parse --abbrev-ref HEAD") - } catch (t: Throwable) { - println(t) - } + try { + gitBranch = runCommand("git rev-parse --abbrev-ref HEAD") + } catch (t: Throwable) { + println(t) + } - return gitBranch + return gitBranch } diff --git a/buildSrc/src/main/kotlin/ProjectTools.kt b/buildSrc/src/main/kotlin/ProjectTools.kt index 06cc3dd6f2..f31ff659f8 100644 --- a/buildSrc/src/main/kotlin/ProjectTools.kt +++ b/buildSrc/src/main/kotlin/ProjectTools.kt @@ -3,14 +3,14 @@ import org.gradle.api.plugins.ExtraPropertiesExtension import org.gradle.kotlin.dsl.getByType class MetadataBuilder { - lateinit var name: String - lateinit var description: String + lateinit var name: String + lateinit var description: String } fun Project.metadata(body: (MetadataBuilder).() -> Unit) { - val builder = MetadataBuilder() + val builder = MetadataBuilder() - body(builder) + body(builder) extensions.getByType().apply { set("pubName", builder.name) diff --git a/buildSrc/src/main/kotlin/disable-explicit-api-mode.gradle.kts b/buildSrc/src/main/kotlin/disable-explicit-api-mode.gradle.kts index be996c1466..d60eae5914 100644 --- a/buildSrc/src/main/kotlin/disable-explicit-api-mode.gradle.kts +++ b/buildSrc/src/main/kotlin/disable-explicit-api-mode.gradle.kts @@ -1,23 +1,23 @@ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode plugins { - kotlin("jvm") + kotlin("jvm") } kotlin { - // https://github.com/JetBrains/kotlin/pull/4598 - fixExplicitApiModeArg() - // We still need to set this, because the IntelliJ Kotlin plugin Inspections - // look for this option instead of the CLI arg - explicitApi = ExplicitApiMode.Disabled + // https://github.com/JetBrains/kotlin/pull/4598 + fixExplicitApiModeArg() + // We still need to set this, because the IntelliJ Kotlin plugin Inspections + // look for this option instead of the CLI arg + explicitApi = ExplicitApiMode.Disabled } fun fixExplicitApiModeArg() { - val clazz = ExplicitApiMode.Disabled.javaClass - val field = clazz.getDeclaredField("cliOption") + val clazz = ExplicitApiMode.Disabled.javaClass + val field = clazz.getDeclaredField("cliOption") - with(field) { - isAccessible = true - set(ExplicitApiMode.Disabled, "disable") - } + with(field) { + isAccessible = true + set(ExplicitApiMode.Disabled, "disable") + } } diff --git a/buildSrc/src/main/kotlin/kordex-module.gradle.kts b/buildSrc/src/main/kotlin/kordex-module.gradle.kts index d3dd40943b..eb56c88ef9 100644 --- a/buildSrc/src/main/kotlin/kordex-module.gradle.kts +++ b/buildSrc/src/main/kotlin/kordex-module.gradle.kts @@ -1,6 +1,4 @@ -import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import java.net.URI plugins { kotlin("jvm") diff --git a/buildSrc/src/main/kotlin/ksp-module.gradle.kts b/buildSrc/src/main/kotlin/ksp-module.gradle.kts index 4888144d6c..2a49b5b737 100644 --- a/buildSrc/src/main/kotlin/ksp-module.gradle.kts +++ b/buildSrc/src/main/kotlin/ksp-module.gradle.kts @@ -1,35 +1,35 @@ plugins { - java - idea - id("com.google.devtools.ksp") + java + idea + id("com.google.devtools.ksp") } tasks { // Hack to get KSP to pick up the module definitions - afterEvaluate { - "kspKotlin" { - if (project.name != "kord-extensions") { - dependsOn(":kord-extensions:build") - } - } - } + afterEvaluate { + "kspKotlin" { + if (project.name != "kord-extensions") { + dependsOn(":kord-extensions:build") + } + } + } } idea { // We use this instead of sourceSets b/c we're all IJ users and this fixes build optimisations - module { - // Not using += due to https://github.com/gradle/gradle/issues/8749 + module { + // Not using += due to https://github.com/gradle/gradle/issues/8749 // (Gradle closed this as fixed, but they broke it again) - sourceDirs = sourceDirs + - file("${layout.buildDirectory}/generated/ksp/main/kotlin") + sourceDirs = sourceDirs + + file("${layout.buildDirectory}/generated/ksp/main/kotlin") - testSources.setFrom( - testSources.from + file("${layout.buildDirectory}/generated/ksp/test/kotlin") - ) + testSources.setFrom( + testSources.from + file("${layout.buildDirectory}/generated/ksp/test/kotlin") + ) // testSourceDirs = testSourceDirs + // file("$buildDir/generated/ksp/test/kotlin") - generatedSourceDirs = generatedSourceDirs + - file("${layout.buildDirectory}/generated/ksp/main/kotlin") + - file("${layout.buildDirectory}/generated/ksp/test/kotlin") - } + generatedSourceDirs = generatedSourceDirs + + file("${layout.buildDirectory}/generated/ksp/main/kotlin") + + file("${layout.buildDirectory}/generated/ksp/test/kotlin") + } } diff --git a/buildSrc/src/main/kotlin/published-module.gradle.kts b/buildSrc/src/main/kotlin/published-module.gradle.kts index c8689bcf3d..736a382989 100644 --- a/buildSrc/src/main/kotlin/published-module.gradle.kts +++ b/buildSrc/src/main/kotlin/published-module.gradle.kts @@ -1,8 +1,6 @@ -import org.gradle.api.publish.maven.MavenPublication - plugins { - `maven-publish` - signing + `maven-publish` + signing } val sourceJar: Task by tasks.getting @@ -10,75 +8,75 @@ val javadocJar: Task by tasks.getting //val dokkaJar: Task by tasks.getting afterEvaluate { - publishing { - repositories { - maven { - name = "Sonatype" - - url = if (project.version.toString().contains("SNAPSHOT")) { - uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") - } else { - uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") - } - - credentials { - username = project.findProperty("ossrhUsername") as String? - ?: System.getenv("OSSRH_USERNAME") - - password = project.findProperty("ossrhPassword") as String? - ?: System.getenv("OSSRH_PASSWORD") - } - - version = project.version - } - } - - publications { - create("maven") { - from(components.getByName("java")) - - artifact(sourceJar) - artifact(javadocJar) + publishing { + repositories { + maven { + name = "Sonatype" + + url = if (project.version.toString().contains("SNAPSHOT")) { + uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + } else { + uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") + } + + credentials { + username = project.findProperty("ossrhUsername") as String? + ?: System.getenv("OSSRH_USERNAME") + + password = project.findProperty("ossrhPassword") as String? + ?: System.getenv("OSSRH_PASSWORD") + } + + version = project.version + } + } + + publications { + create("maven") { + from(components.getByName("java")) + + artifact(sourceJar) + artifact(javadocJar) // artifact(dokkaJar) - pom { - name.set(project.ext.get("pubName").toString()) - description.set(project.ext.get("pubDesc").toString()) - - url.set("https://kordex.kotlindiscord.com") - - packaging = "jar" - - scm { - connection.set("scm:git:https://github.com/Kord-Extensions/kord-extensions.git") - developerConnection.set("scm:git:git@github.com:Kord-Extensions/kord-extensions.git") - url.set("https://github.com/Kord-Extensions/kord-extensions.git") - } - - licenses { - license { - name.set("Mozilla Public License Version 2.0") - url.set("https://www.mozilla.org/en-US/MPL/2.0/") - } - } - - developers { - developer { - id.set("gdude2002") - name.set("Gareth Coles") - } - } - } - } - } - } - - signing { - val signingKey: String? by project ?: return@signing - val signingPassword: String? by project ?: return@signing - - useInMemoryPgpKeys(signingKey, signingPassword) - - sign(publishing.publications["maven"]) - } + pom { + name.set(project.ext.get("pubName").toString()) + description.set(project.ext.get("pubDesc").toString()) + + url.set("https://kordex.kotlindiscord.com") + + packaging = "jar" + + scm { + connection.set("scm:git:https://github.com/Kord-Extensions/kord-extensions.git") + developerConnection.set("scm:git:git@github.com:Kord-Extensions/kord-extensions.git") + url.set("https://github.com/Kord-Extensions/kord-extensions.git") + } + + licenses { + license { + name.set("Mozilla Public License Version 2.0") + url.set("https://www.mozilla.org/en-US/MPL/2.0/") + } + } + + developers { + developer { + id.set("gdude2002") + name.set("Gareth Coles") + } + } + } + } + } + } + + signing { + val signingKey: String? by project ?: return@signing + val signingPassword: String? by project ?: return@signing + + useInMemoryPgpKeys(signingKey, signingPassword) + + sign(publishing.publications["maven"]) + } } diff --git a/buildSrc/src/main/kotlin/tested-module.gradle.kts b/buildSrc/src/main/kotlin/tested-module.gradle.kts index 0530c19976..e56153f666 100644 --- a/buildSrc/src/main/kotlin/tested-module.gradle.kts +++ b/buildSrc/src/main/kotlin/tested-module.gradle.kts @@ -1,17 +1,17 @@ plugins { - java + java } tasks { - test { - useJUnitPlatform() + test { + useJUnitPlatform() - testLogging.showStandardStreams = true + testLogging.showStandardStreams = true - testLogging { - events("PASSED", "FAILED", "SKIPPED", "STANDARD_OUT", "STANDARD_ERROR") - } + testLogging { + events("PASSED", "FAILED", "SKIPPED", "STANDARD_OUT", "STANDARD_ERROR") + } - systemProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug") - } + systemProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug") + } } diff --git a/data-adapters/adapter-mongodb/README.md b/data-adapters/adapter-mongodb/README.md index 87253bd685..707bfde60f 100644 --- a/data-adapters/adapter-mongodb/README.md +++ b/data-adapters/adapter-mongodb/README.md @@ -13,8 +13,8 @@ To switch to the MongoDB data adapter follow these steps: 1. Add the MongoDB dependencies to your project. You can get the latest version number [from Maven Central](https://mvnrepository.com/artifact/org.mongodb/mongodb-driver-kotlin-sync): - - `org.mongodb:mongodb-driver-kotlin-coroutine` - - `org.mongodb:bson-kotlinx` + - `org.mongodb:mongodb-driver-kotlin-coroutine` + - `org.mongodb:bson-kotlinx` 2. Set the `ADAPTER_MONGODB_URI` environmental variable to a MongoDB connection string. 3. Use the `mongoDB` function to set up the data adapter. @@ -29,7 +29,7 @@ To switch to the MongoDB data adapter follow these steps: ``` 4. If you use MongoDB elsewhere in your project, you can use the provided codecs to handle these types: - - `DateTimePeriod` (kotlinx Datetime) + - `DateTimePeriod` (kotlinx Datetime) - `Instant` (Kotlinx Datetime) - `Snowflake` (Kord) - `StorageType` (KordEx) diff --git a/data-adapters/adapter-mongodb/build.gradle.kts b/data-adapters/adapter-mongodb/build.gradle.kts index f7381e2e4c..83d3b5597f 100644 --- a/data-adapters/adapter-mongodb/build.gradle.kts +++ b/data-adapters/adapter-mongodb/build.gradle.kts @@ -1,31 +1,31 @@ plugins { - `kordex-module` - `published-module` + `kordex-module` + `published-module` } metadata { - name = "KordEx Adapters: MongoDB" - description = "KordEx data adapter for MongoDB, including extra codecs" + name = "KordEx Adapters: MongoDB" + description = "KordEx data adapter for MongoDB, including extra codecs" } repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } } dependencies { - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - implementation(libs.kotlin.stdlib) - implementation(libs.kx.coro) - implementation(libs.bundles.logging) - implementation(libs.mongodb) - implementation(libs.mongodb.bson.kotlinx) + implementation(libs.kotlin.stdlib) + implementation(libs.kx.coro) + implementation(libs.bundles.logging) + implementation(libs.mongodb) + implementation(libs.mongodb.bson.kotlinx) - implementation(project(":kord-extensions")) + implementation(project(":kord-extensions")) } group = "com.kotlindiscord.kord.extensions" diff --git a/detekt.yml b/detekt.yml index f440e69d9e..bc726a2aef 100644 --- a/detekt.yml +++ b/detekt.yml @@ -2,10 +2,10 @@ build: maxIssues: 0 excludeCorrectable: false weights: - # complexity: 2 - # LongParameterList: 1 - # style: 1 - # comments: 1 + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 config: validation: true @@ -25,9 +25,9 @@ processors: console-reports: active: true exclude: - - 'ProjectStatisticsReport' - - 'NotificationReport' - - 'FileBasedFindingsReport' + - 'ProjectStatisticsReport' + - 'NotificationReport' + - 'FileBasedFindingsReport' output-reports: active: true @@ -38,7 +38,7 @@ output-reports: comments: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] AbsentOrWrongFileLicense: active: false licenseTemplateFile: 'license.template' @@ -76,10 +76,10 @@ complexity: ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false ignoreNestingFunctions: false - nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull] + nestingFunctions: [ run, let, apply, with, also, use, forEach, isNotNull, ifNull ] LabeledExpression: active: false - ignoredLabels: [] + ignoredLabels: [ ] LargeClass: active: false threshold: 600 @@ -92,7 +92,7 @@ complexity: constructorThreshold: 7 ignoreDefaultParameters: false ignoreDataClasses: true - ignoreAnnotated: [] + ignoreAnnotated: [ ] MethodOverloading: active: false threshold: 6 @@ -103,14 +103,14 @@ complexity: active: true StringLiteralDuplication: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] thresholdInFiles: 11 thresholdInClasses: 11 thresholdInInterfaces: 11 @@ -168,10 +168,10 @@ exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: true - methodNames: [toString, hashCode, equals, finalize] + methodNames: [ toString, hashCode, equals, finalize ] InstanceOfCheckForException: active: false # It does often make things more readable - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] NotImplementedDeclaration: active: false PrintStackTrace: @@ -184,10 +184,10 @@ exceptions: SwallowedException: active: false ignoredExceptionTypes: - - InterruptedException - - NumberFormatException - - ParseException - - MalformedURLException + - InterruptedException + - NumberFormatException + - ParseException + - MalformedURLException allowedExceptionNameRegex: '^(_|(ignore|expected).*)' ThrowingExceptionFromFinally: active: true @@ -196,31 +196,31 @@ exceptions: ThrowingExceptionsWithoutMessageOrCause: active: true exceptions: - - IllegalArgumentException - - IllegalStateException - - IOException + - IllegalArgumentException + - IllegalStateException + - IOException ThrowingNewInstanceOfSameException: active: true TooGenericExceptionCaught: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] exceptionNames: - - ArrayIndexOutOfBoundsException - - Error - - Exception - - IllegalMonitorStateException - - NullPointerException - - IndexOutOfBoundsException - - RuntimeException - - Throwable + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable allowedExceptionNameRegex: '^(_|(ignore|expected).*)' TooGenericExceptionThrown: active: true exceptionNames: - - Error - - Exception - - Throwable - - RuntimeException + - Error + - Exception + - Throwable + - RuntimeException formatting: active: true @@ -348,40 +348,40 @@ naming: active: true ClassNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] classPattern: '[A-Z$][a-zA-Z0-9$]*' ConstructorParameterNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] parameterPattern: '[a-z][A-Za-z0-9]*' privateParameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' ignoreOverridden: true EnumNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] - forbiddenName: [] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] + forbiddenName: [ ] FunctionMaxLength: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] maximumFunctionNameLength: 30 FunctionMinLength: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] minimumFunctionNameLength: 3 FunctionNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' excludeClassPattern: '$^' ignoreOverridden: true FunctionParameterNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' ignoreOverridden: true @@ -398,31 +398,31 @@ naming: active: true ObjectPropertyNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] constantPattern: '[A-Za-z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' PackageNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' TopLevelPropertyNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] constantPattern: '[A-Z][_A-Z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' VariableMaxLength: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] maximumVariableNameLength: 64 VariableMinLength: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] minimumVariableNameLength: 1 VariableNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' @@ -434,10 +434,10 @@ performance: active: true ForEachOnRange: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] SpreadOperator: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] UnnecessaryTemporaryInstantiation: active: true @@ -468,8 +468,8 @@ potential-bugs: active: true LateinitUsage: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] - ignoreAnnotated: [] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] + ignoreAnnotated: [ ] ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: active: true @@ -500,7 +500,7 @@ style: active: true DataClassContainsFunctions: active: true - conversionFunctionPrefix: ['to'] + conversionFunctionPrefix: [ 'to' ] DataClassShouldBeImmutable: active: true EqualsNullCall: @@ -516,15 +516,15 @@ style: includeLineWrapping: false ForbiddenComment: active: false - values: ['TODO:', 'FIXME:', 'STOPSHIP:'] + values: [ 'TODO:', 'FIXME:', 'STOPSHIP:' ] allowedPatterns: '' ForbiddenImport: active: false - imports: [] + imports: [ ] forbiddenPatterns: '' ForbiddenMethodCall: active: false - methods: [] + methods: [ ] ForbiddenVoid: active: true ignoreOverridden: true @@ -532,15 +532,15 @@ style: FunctionOnlyReturningConstant: active: true ignoreOverridableFunction: true - excludedFunctions: ['describeContents'] - ignoreAnnotated: ['dagger.Provides'] + excludedFunctions: [ 'describeContents' ] + ignoreAnnotated: [ 'dagger.Provides' ] LoopWithTooManyJumpStatements: active: true maxJumpCount: 3 MagicNumber: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] - ignoreNumbers: ['-1', '0', '1', '2'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] + ignoreNumbers: [ '-1', '0', '1', '2' ] ignoreHashCodeFunction: true ignorePropertyDeclaration: false ignoreLocalVariableDeclaration: false @@ -587,7 +587,7 @@ style: ReturnCount: active: false max: 2 - excludedFunctions: ['equals'] + excludedFunctions: [ 'equals' ] excludeLabeled: false excludeReturnFromLambda: true excludeGuardClauses: false @@ -607,7 +607,7 @@ style: acceptableLength: 5 UnnecessaryAbstractClass: active: true - ignoreAnnotated: ['dagger.Module'] + ignoreAnnotated: [ 'dagger.Module' ] UnnecessaryAnnotationUseSiteTarget: active: true UnnecessaryApply: @@ -635,7 +635,7 @@ style: active: true UseDataClass: active: true - ignoreAnnotated: [] + ignoreAnnotated: [ ] allowVars: false UseEmptyCounterpart: active: true @@ -653,8 +653,8 @@ style: active: true WildcardImport: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] - excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] + excludeImports: [ 'java.util.*', 'kotlinx.android.synthetic.*' ] libraries: ForbiddenPublicDataClass: diff --git a/extra-modules/extra-mappings/README.md b/extra-modules/extra-mappings/README.md index 48424c58c6..52ee1a32c1 100644 --- a/extra-modules/extra-mappings/README.md +++ b/extra-modules/extra-mappings/README.md @@ -13,15 +13,16 @@ If you're looking for older versions (and older tags), you can find them # Getting Started -* **Maven repo:** Maven Central for releases, `https://s01.oss.sonatype.org/content/repositories/snapshots` for snapshots +* **Maven repo:** Maven Central for releases, `https://s01.oss.sonatype.org/content/repositories/snapshots` for + snapshots * **Maven coordinates:** `com.kotlindiscord.kord.extensions:extra-mappings:VERSION` * Some extra Maven repos are required: - * **FabricMC**: `https://maven.fabricmc.net` - * **JitPack**: `https://jitpack.io` - * **QuiltMC (Releases)**: `https://maven.quiltmc.org/repository/release` - * **QuiltMC (Snapshots)**: `https://maven.quiltmc.org/repository/snapshot` - * **Shedaniel**: `https://maven.shedaniel.me` + * **FabricMC**: `https://maven.fabricmc.net` + * **JitPack**: `https://jitpack.io` + * **QuiltMC (Releases)**: `https://maven.quiltmc.org/repository/release` + * **QuiltMC (Snapshots)**: `https://maven.quiltmc.org/repository/snapshot` + * **Shedaniel**: `https://maven.shedaniel.me` At its simplest, you can add this extension directly to your bot with no further configuration. For example: diff --git a/extra-modules/extra-mappings/build.gradle.kts b/extra-modules/extra-mappings/build.gradle.kts index 9028668ff7..bc38138e82 100644 --- a/extra-modules/extra-mappings/build.gradle.kts +++ b/extra-modules/extra-mappings/build.gradle.kts @@ -1,65 +1,65 @@ plugins { - `kordex-module` - `published-module` - `disable-explicit-api-mode` - `ksp-module` + `kordex-module` + `published-module` + `disable-explicit-api-mode` + `ksp-module` } metadata { - name = "KordEx Extra: Mappings" - description = "KordEx extra module that provides Minecraft mappings functionality for bots" + name = "KordEx Extra: Mappings" + description = "KordEx extra module that provides Minecraft mappings functionality for bots" } repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } - maven { - name = "FabricMC" - url = uri("https://maven.fabricmc.net/") - } + maven { + name = "FabricMC" + url = uri("https://maven.fabricmc.net/") + } - maven { - name = "QuiltMC (Releases)" - url = uri("https://maven.quiltmc.org/repository/release/") - } + maven { + name = "QuiltMC (Releases)" + url = uri("https://maven.quiltmc.org/repository/release/") + } - maven { - name = "QuiltMC (Snapshots)" - url = uri("https://maven.quiltmc.org/repository/snapshot/") - } + maven { + name = "QuiltMC (Snapshots)" + url = uri("https://maven.quiltmc.org/repository/snapshot/") + } - maven { - name = "Shedaniel" - url = uri("https://maven.shedaniel.me") - } + maven { + name = "Shedaniel" + url = uri("https://maven.shedaniel.me") + } - maven { - name = "JitPack" - url = uri("https://jitpack.io") - } + maven { + name = "JitPack" + url = uri("https://jitpack.io") + } } dependencies { - api(libs.linkie) + api(libs.linkie) - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - implementation(libs.bundles.logging) - implementation(libs.kotlin.stdlib) + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) - testImplementation(libs.groovy) // For logback config - testImplementation(libs.jansi) - testImplementation(libs.logback) - testImplementation(libs.logback.groovy) + testImplementation(libs.groovy) // For logback config + testImplementation(libs.jansi) + testImplementation(libs.logback) + testImplementation(libs.logback.groovy) - implementation(project(":kord-extensions")) - implementation(project(":annotations")) + implementation(project(":kord-extensions")) + implementation(project(":annotations")) - ksp(project(":annotation-processor")) + ksp(project(":annotation-processor")) } group = "com.kotlindiscord.kord.extensions" diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Checks.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Checks.kt index 2898845009..1adb73804c 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Checks.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Checks.kt @@ -33,61 +33,61 @@ import io.github.oshai.kotlinlogging.KotlinLogging * @param banned List of banned category IDs */ suspend fun CheckContext.allowedCategory( - allowed: List, - banned: List + allowed: List, + banned: List, ) { - val logger = KotlinLogging.logger { } - val channel = channelFor(event) - - if (channel == null) { - logger.trace { "Passing: Event is not channel-related" } - - pass() - } else if (channel !is CategorizableChannel) { - logger.trace { "Passing: Channel is not categorizable (eg, it's a DM)" } - - pass() - } else { - val parent = channel.category - - if (allowed.isNotEmpty()) { - if (parent == null) { - logger.debug { "Failing: We have allowed categories, but the command was sent outside of a category" } - - fail() - } else if (allowed.contains(parent.id)) { - logger.trace { "Passing: Event happened in an allowed category" } - - pass() - } else { - logger.debug { "Failing: Event happened outside of the allowed categories" } - - fail() - } - } else { - if (parent == null) { - logger.trace { - "Passing: We have no allowed categories, and the command was sent outside of a category" - } - - pass() - } else if (banned.isNotEmpty()) { - if (!banned.contains(parent.id)) { - logger.trace { "Passing: Event did not happen in a banned category" } - - pass() - } else { - logger.debug { "Failing: Event happened in a banned category" } - - fail() - } - } else { - logger.trace { "Passing: No allowed or banned categories configured" } - - pass() - } - } - } + val logger = KotlinLogging.logger { } + val channel = channelFor(event) + + if (channel == null) { + logger.trace { "Passing: Event is not channel-related" } + + pass() + } else if (channel !is CategorizableChannel) { + logger.trace { "Passing: Channel is not categorizable (eg, it's a DM)" } + + pass() + } else { + val parent = channel.category + + if (allowed.isNotEmpty()) { + if (parent == null) { + logger.debug { "Failing: We have allowed categories, but the command was sent outside of a category" } + + fail() + } else if (allowed.contains(parent.id)) { + logger.trace { "Passing: Event happened in an allowed category" } + + pass() + } else { + logger.debug { "Failing: Event happened outside of the allowed categories" } + + fail() + } + } else { + if (parent == null) { + logger.trace { + "Passing: We have no allowed categories, and the command was sent outside of a category" + } + + pass() + } else if (banned.isNotEmpty()) { + if (!banned.contains(parent.id)) { + logger.trace { "Passing: Event did not happen in a banned category" } + + pass() + } else { + logger.debug { "Failing: Event happened in a banned category" } + + fail() + } + } else { + logger.trace { "Passing: No allowed or banned categories configured" } + + pass() + } + } + } } /** @@ -107,45 +107,45 @@ suspend fun CheckContext.allowedCategory * @param banned List of banned channel IDs */ suspend fun CheckContext.allowedChannel( - allowed: List, - banned: List + allowed: List, + banned: List, ) { - val logger = KotlinLogging.logger { } - val channel = channelFor(event) - - if (channel == null) { - logger.trace { "Passing: Event is not channel-related" } - - pass() - } else if (channel !is GuildChannel) { - logger.trace { "Passing: Command was sent privately" } - - pass() // It's a DM - } else if (allowed.isNotEmpty()) { - if (allowed.contains(channel.id)) { - logger.trace { "Passing: Event happened in an allowed channel" } - - pass() - } else { - logger.debug { "Failing: Event did not happen in an allowed channel" } - - fail() - } - } else if (banned.isNotEmpty()) { - if (!banned.contains(channel.id)) { - logger.trace { "Passing: Event did not happen in a banned channel" } - - pass() - } else { - logger.debug { "Failing: Event happened in a banned channel" } - - fail() - } - } else { - logger.trace { "Passing: No allowed or banned channels configured" } - - pass() - } + val logger = KotlinLogging.logger { } + val channel = channelFor(event) + + if (channel == null) { + logger.trace { "Passing: Event is not channel-related" } + + pass() + } else if (channel !is GuildChannel) { + logger.trace { "Passing: Command was sent privately" } + + pass() // It's a DM + } else if (allowed.isNotEmpty()) { + if (allowed.contains(channel.id)) { + logger.trace { "Passing: Event happened in an allowed channel" } + + pass() + } else { + logger.debug { "Failing: Event did not happen in an allowed channel" } + + fail() + } + } else if (banned.isNotEmpty()) { + if (!banned.contains(channel.id)) { + logger.trace { "Passing: Event did not happen in a banned channel" } + + pass() + } else { + logger.debug { "Failing: Event happened in a banned channel" } + + fail() + } + } else { + logger.trace { "Passing: No allowed or banned channels configured" } + + pass() + } } /** @@ -164,39 +164,39 @@ suspend fun CheckContext.allowedChannel( * @param banned List of banned guild IDs */ suspend fun CheckContext.allowedGuild( - allowed: List, - banned: List + allowed: List, + banned: List, ) { - val logger = KotlinLogging.logger { } - val guild = guildFor(event) - - if (guild == null) { - logger.trace { "Passing: Event is not guild-related" } - - pass() - } else if (allowed.isNotEmpty()) { - if (allowed.contains(guild.id)) { - logger.trace { "Passing: Event happened in an allowed guild" } - - pass() - } else { - logger.debug { "Failing: Event did not happen in an allowed guild" } - - fail() - } - } else if (banned.isNotEmpty()) { - if (!banned.contains(guild.id)) { - logger.trace { "Passing: Event did not happen in a banned guild" } - - pass() - } else { - logger.debug { "Failing: Event happened in a banned guild" } - - fail() - } - } else { - logger.trace { "Passing: No allowed or banned guilds configured" } - - pass() - } + val logger = KotlinLogging.logger { } + val guild = guildFor(event) + + if (guild == null) { + logger.trace { "Passing: Event is not guild-related" } + + pass() + } else if (allowed.isNotEmpty()) { + if (allowed.contains(guild.id)) { + logger.trace { "Passing: Event happened in an allowed guild" } + + pass() + } else { + logger.debug { "Failing: Event did not happen in an allowed guild" } + + fail() + } + } else if (banned.isNotEmpty()) { + if (!banned.contains(guild.id)) { + logger.trace { "Passing: Event did not happen in a banned guild" } + + pass() + } else { + logger.debug { "Failing: Event happened in a banned guild" } + + fail() + } + } else { + logger.trace { "Passing: No allowed or banned guilds configured" } + + pass() + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Functions.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Functions.kt index eb8ce4c5ab..bb13494769 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Functions.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Functions.kt @@ -13,10 +13,10 @@ import com.kotlindiscord.kord.extensions.modules.extra.mappings.builders.ExtMapp * Configure the mappings extension and add it to the bot. */ fun ExtensibleBotBuilder.ExtensionsBuilder.extMappings(builder: ExtMappingsBuilder.() -> Unit) { - val obj = ExtMappingsBuilder() + val obj = ExtMappingsBuilder() - builder(obj) - MappingsExtension.configure(obj) + builder(obj) + MappingsExtension.configure(obj) - add(::MappingsExtension) + add(::MappingsExtension) } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/BarnArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/BarnArguments.kt index 3d8d8e5a88..06300431da 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/BarnArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/BarnArguments.kt @@ -12,9 +12,9 @@ import me.shedaniel.linkie.namespaces.BarnNamespace /** Arguments for Barn mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class BarnArguments : MappingArguments(BarnNamespace), IntermediaryMappable { - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of intermediary" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of intermediary" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/FeatherArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/FeatherArguments.kt index 568c594a3f..fbfd32dc9c 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/FeatherArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/FeatherArguments.kt @@ -12,9 +12,9 @@ import me.shedaniel.linkie.namespaces.FeatherNamespace /** Arguments for Feather mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class FeatherArguments : MappingArguments(FeatherNamespace), IntermediaryMappable { - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of Calamus" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of Calamus" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/HashedMojangArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/HashedMojangArguments.kt index e2f81e23c7..2993043c63 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/HashedMojangArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/HashedMojangArguments.kt @@ -14,15 +14,15 @@ import me.shedaniel.linkie.namespaces.MojangHashedNamespace /** Arguments for hashed Mojang mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class HashedMojangArguments : MappingWithChannelArguments(MojangHashedNamespace), IntermediaryMappable { - override val channel by optionalEnumChoice { - name = "channel" - description = "Mappings channel to use for this query" - typeName = "official/snapshot" - } + override val channel by optionalEnumChoice { + name = "channel" + description = "Mappings channel to use for this query" + typeName = "official/snapshot" + } - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of intermediary/hashed" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of intermediary/hashed" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/IntermediaryMappable.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/IntermediaryMappable.kt index 81ff3243aa..400cc32848 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/IntermediaryMappable.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/IntermediaryMappable.kt @@ -11,8 +11,8 @@ package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments * and method descriptors to intermediary names. */ interface IntermediaryMappable { - /** - * Whether the results should map to named instead of intermediary/hashed. - */ - val mapDescriptors: Boolean + /** + * Whether the results should map to named instead of intermediary/hashed. + */ + val mapDescriptors: Boolean } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/LegacyYarnArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/LegacyYarnArguments.kt index a59232ff31..0d90d73fa2 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/LegacyYarnArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/LegacyYarnArguments.kt @@ -12,9 +12,9 @@ import me.shedaniel.linkie.namespaces.LegacyYarnNamespace /** Arguments for Legacy Yarn mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class LegacyYarnArguments : MappingArguments(LegacyYarnNamespace), IntermediaryMappable { - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of intermediary/hashed" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of intermediary/hashed" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingArguments.kt index ff2e097694..57ab4b06e8 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingArguments.kt @@ -17,19 +17,19 @@ import me.shedaniel.linkie.Namespace */ @Suppress("UndocumentedPublicProperty") open class MappingArguments(val namespace: Namespace) : Arguments() { - private val versions by lazy { namespace.getAllSortedVersions() } + private val versions by lazy { namespace.getAllSortedVersions() } - val query by string { - name = "query" - description = "Name to query mappings for" - } + val query by string { + name = "query" + description = "Name to query mappings for" + } - val version by optionalMappingsVersion { - name = "version" - description = "Minecraft version to use for this query" + val version by optionalMappingsVersion { + name = "version" + description = "Minecraft version to use for this query" - namespace(namespace) + namespace(namespace) - autocompleteVersions { versions } - } + autocompleteVersions { versions } + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingConversionArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingConversionArguments.kt index d3a5182b83..77e903b54e 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingConversionArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingConversionArguments.kt @@ -22,16 +22,16 @@ import me.shedaniel.linkie.utils.tryToVersion */ @Suppress("UndocumentedPublicProperty") class MappingConversionArguments(enabledNamespaces: suspend (Snowflake?) -> Map) : Arguments() { - val query by string { - name = "query" - description = "Name to query mappings for" - } + val query by string { + name = "query" + description = "Name to query mappings for" + } - val inputNamespace by string { - name = "input" - description = "The namespace to convert from" + val inputNamespace by string { + name = "input" + description = "The namespace to convert from" - autoComplete { + autoComplete { val guildId = command.data.guildId.value val values = enabledNamespaces(guildId) suggestStringMap(values) @@ -43,9 +43,9 @@ class MappingConversionArguments(enabledNamespaces: suspend (Snowflake?) -> Map< context.getGuild() != null && value !in enabledNamespaces(context.getGuild()!!.id) } } - } + } - val outputNamespace by string { + val outputNamespace by string { name = "output" description = "The namespace to convert to" @@ -63,34 +63,34 @@ class MappingConversionArguments(enabledNamespaces: suspend (Snowflake?) -> Map< } } - val version by optionalString { - name = "version" - description = "Minecraft version to use for this query" + val version by optionalString { + name = "version" + description = "Minecraft version to use for this query" - autocompleteVersions { - val inputNamespace = command.options["input"]?.value?.toString()?.toNamespace() - val outputNamespace = command.options["output"]?.value?.toString()?.toNamespace() + autocompleteVersions { + val inputNamespace = command.options["input"]?.value?.toString()?.toNamespace() + val outputNamespace = command.options["output"]?.value?.toString()?.toNamespace() - if (inputNamespace == null || outputNamespace == null) { - emptyList() - } else { - inputNamespace.getAllVersions().toSet().intersect(outputNamespace.getAllVersions().toSet()) + if (inputNamespace == null || outputNamespace == null) { + emptyList() + } else { + inputNamespace.getAllVersions().toSet().intersect(outputNamespace.getAllVersions().toSet()) .sortedByDescending { it.tryToVersion() } - } - } - } + } + } + } - val inputChannel by optionalEnumChoice { - name = "input-channel" - description = "The mappings channel to use for input" + val inputChannel by optionalEnumChoice { + name = "input-channel" + description = "The mappings channel to use for input" - typeName = "official/snapshot" - } + typeName = "official/snapshot" + } - val outputChannel by optionalEnumChoice { - name = "output-channel" - description = "The mappings channel to use for output" + val outputChannel by optionalEnumChoice { + name = "output-channel" + description = "The mappings channel to use for output" - typeName = "official/snapshot" - } + typeName = "official/snapshot" + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingWithChannelArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingWithChannelArguments.kt index e037a56479..4deecb8427 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingWithChannelArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MappingWithChannelArguments.kt @@ -12,5 +12,5 @@ import me.shedaniel.linkie.Namespace /** An argument base which provides an argument for a mapping channel. **/ @Suppress("UndocumentedPublicProperty") abstract class MappingWithChannelArguments(namespace: Namespace) : MappingArguments(namespace) { - abstract val channel: ChoiceEnum? + abstract val channel: ChoiceEnum? } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MojangArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MojangArguments.kt index 8922d148e1..4776bde154 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MojangArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MojangArguments.kt @@ -14,15 +14,15 @@ import me.shedaniel.linkie.namespaces.MojangNamespace /** Arguments for Mojang mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class MojangArguments : MappingWithChannelArguments(MojangNamespace), IntermediaryMappable { - override val channel by optionalEnumChoice { - name = "channel" - description = "Mappings channel to use for this query" - typeName = "official/snapshot" - } + override val channel by optionalEnumChoice { + name = "channel" + description = "Mappings channel to use for this query" + typeName = "official/snapshot" + } - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of intermediary/hashed" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of intermediary/hashed" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/PlasmaArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/PlasmaArguments.kt index 53f14637d6..44d02bfabb 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/PlasmaArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/PlasmaArguments.kt @@ -12,9 +12,9 @@ import me.shedaniel.linkie.namespaces.PlasmaNamespace /** Arguments for Plasma mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class PlasmaArguments : MappingArguments(PlasmaNamespace), IntermediaryMappable { - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of intermediary/hashed" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of intermediary/hashed" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/QuiltArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/QuiltArguments.kt index 172ece65f6..cad3f2e6ed 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/QuiltArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/QuiltArguments.kt @@ -14,9 +14,9 @@ import me.shedaniel.linkie.namespaces.QuiltMappingsNamespace */ @Suppress("UndocumentedPublicProperty") class QuiltArguments : MappingArguments(QuiltMappingsNamespace), IntermediaryMappable { - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of hashed" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of hashed" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/SrgMojangArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/SrgMojangArguments.kt index ca6563efaa..15d8b80c93 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/SrgMojangArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/SrgMojangArguments.kt @@ -12,9 +12,9 @@ import me.shedaniel.linkie.namespaces.MojangSrgNamespace /** Arguments for SRG Mojang mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class SrgMojangArguments : MappingArguments(MojangSrgNamespace), IntermediaryMappable { - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of SRG" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of SRG" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarnArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarnArguments.kt index c99fbc771b..28c861b37e 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarnArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarnArguments.kt @@ -14,16 +14,16 @@ import me.shedaniel.linkie.namespaces.YarnNamespace /** Arguments for Yarn mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class YarnArguments : MappingWithChannelArguments(YarnNamespace), IntermediaryMappable { - override val channel by optionalEnumChoice { - name = "channel" - description = "Mappings channel to use for this query" + override val channel by optionalEnumChoice { + name = "channel" + description = "Mappings channel to use for this query" - typeName = "official/snapshot" - } + typeName = "official/snapshot" + } - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of intermediary/hashed" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of intermediary/hashed" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarrnArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarrnArguments.kt index 2d33977a6c..7e7417d8b0 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarrnArguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarrnArguments.kt @@ -12,9 +12,9 @@ import me.shedaniel.linkie.namespaces.YarrnNamespace /** Arguments for Yarrn (Yarn for Infdev) mappings lookup commands. **/ @Suppress("UndocumentedPublicProperty") class YarrnArguments : MappingArguments(YarrnNamespace), IntermediaryMappable { - override val mapDescriptors by defaultingBoolean { - name = "map-descriptor" - description = "Whether to map field/method descriptors to named instead of intermediary/hashed" - defaultValue = true - } + override val mapDescriptors by defaultingBoolean { + name = "map-descriptor" + description = "Whether to map field/method descriptors to named instead of intermediary/hashed" + defaultValue = true + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt index 521fd44fae..4df951edba 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt @@ -11,19 +11,19 @@ import me.shedaniel.linkie.Namespace /** Builder used for configuring the mappings extension. **/ class ExtMappingsBuilder { - /** List of checks to apply against the name of the command. **/ - val commandChecks: MutableList SlashCommandCheck> = mutableListOf() + /** List of checks to apply against the name of the command. **/ + val commandChecks: MutableList SlashCommandCheck> = mutableListOf() - /** List of checks to apply against the namespace corresponding with the command. **/ - val namespaceChecks: MutableList SlashCommandCheck> = mutableListOf() + /** List of checks to apply against the namespace corresponding with the command. **/ + val namespaceChecks: MutableList SlashCommandCheck> = mutableListOf() - /** Register a check that applies against the name of a command, and its message creation event. **/ - fun commandCheck(check: suspend (String) -> SlashCommandCheck) { - commandChecks.add(check) - } + /** Register a check that applies against the name of a command, and its message creation event. **/ + fun commandCheck(check: suspend (String) -> SlashCommandCheck) { + commandChecks.add(check) + } - /** Register a check that applies against the mappings namespace for a command, and its message creation event. **/ - fun namespaceCheck(check: suspend (Namespace) -> SlashCommandCheck) { - namespaceChecks.add(check) - } + /** Register a check that applies against the mappings namespace for a command, and its message creation event. **/ + fun namespaceCheck(check: suspend (Namespace) -> SlashCommandCheck) { + namespaceChecks.add(check) + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/converters/MappingsVersionConverter.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/converters/MappingsVersionConverter.kt index b69bc7544d..522ce856a6 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/converters/MappingsVersionConverter.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/converters/MappingsVersionConverter.kt @@ -31,55 +31,55 @@ import me.shedaniel.linkie.Namespace * Argument converter for [MappingsContainer] objects based on mappings versions. */ @Converter( - "mappingsVersion", - types = [ConverterType.SINGLE, ConverterType.OPTIONAL], - imports = ["me.shedaniel.linkie.Namespace"], + "mappingsVersion", + types = [ConverterType.SINGLE, ConverterType.OPTIONAL], + imports = ["me.shedaniel.linkie.Namespace"], - builderFields = ["public lateinit var namespaceGetter: suspend () -> Namespace"], - builderExtraStatements = [ - "/** Convenience function for setting the namespace getter to a specific namespace. **/", - "public fun namespace(namespace: Namespace) {", - " namespaceGetter = { namespace }", - "}", - ] + builderFields = ["public lateinit var namespaceGetter: suspend () -> Namespace"], + builderExtraStatements = [ + "/** Convenience function for setting the namespace getter to a specific namespace. **/", + "public fun namespace(namespace: Namespace) {", + " namespaceGetter = { namespace }", + "}", + ] ) class MappingsVersionConverter( - private val namespaceGetter: suspend () -> Namespace, - override var validator: Validator = null + private val namespaceGetter: suspend () -> Namespace, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "version" - override val showTypeInSignature: Boolean = false + override val signatureTypeString: String = "version" + override val showTypeInSignature: Boolean = false - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false - return parse(arg) - } + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false + return parse(arg) + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue: String = (option as? StringOptionValue)?.value ?: return false - return parse(optionValue) - } + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue: String = (option as? StringOptionValue)?.value ?: return false + return parse(optionValue) + } - private suspend fun parse(string: String): Boolean { - newSingleThreadContext("version-parser").use { context -> - return withContext(context) { - val namespace: Namespace = namespaceGetter.invoke() + private suspend fun parse(string: String): Boolean { + newSingleThreadContext("version-parser").use { context -> + return withContext(context) { + val namespace: Namespace = namespaceGetter.invoke() - if (string in namespace.getAllVersions()) { - val version: MappingsContainer? = namespace.getProvider(string).getOrNull() + if (string in namespace.getAllVersions()) { + val version: MappingsContainer? = namespace.getProvider(string).getOrNull() - if (version != null) { - this@MappingsVersionConverter.parsed = version + if (version != null) { + this@MappingsVersionConverter.parsed = version - return@withContext true - } - } + return@withContext true + } + } - throw DiscordRelayedException("Invalid ${namespace.id} version: `$string`") - } - } - } + throw DiscordRelayedException("Invalid ${namespace.id} version: `$string`") + } + } + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/enums/Channels.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/enums/Channels.kt index f072f53127..5d349ad78d 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/enums/Channels.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/enums/Channels.kt @@ -14,6 +14,6 @@ import com.kotlindiscord.kord.extensions.commands.application.slash.converters.C * @property readableName String name used for the channel by Linkie */ enum class Channels(override val readableName: String) : ChoiceEnum { - OFFICIAL("official"), - SNAPSHOT("snapshot") + OFFICIAL("official"), + SNAPSHOT("snapshot") } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/exceptions/UnsupportedNamespaceException.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/exceptions/UnsupportedNamespaceException.kt index 8ff29eb962..72dfa9b1aa 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/exceptions/UnsupportedNamespaceException.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/exceptions/UnsupportedNamespaceException.kt @@ -12,5 +12,5 @@ package com.kotlindiscord.kord.extensions.modules.extra.mappings.exceptions * @property namespace The invalid namespace. **/ class UnsupportedNamespaceException(val namespace: String) : Exception( - "Unknown/unsupported namespace: $namespace" + "Unknown/unsupported namespace: $namespace" ) diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/Arguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/Arguments.kt index 9b2b2fd9c8..6074fe0330 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/Arguments.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/Arguments.kt @@ -24,18 +24,18 @@ internal const val MAX_RESULTS = 25 // set by Discord's API * [versions] is a supplier of the list of strings to autocomplete from. */ inline fun ConverterBuilder.autocompleteVersions( - crossinline versions: AutoCompleteInteraction.() -> List + crossinline versions: AutoCompleteInteraction.() -> List, ) { - autoComplete { - val partiallyTyped = focusedOption.value as? String + autoComplete { + val partiallyTyped = focusedOption.value as? String - val map = versions() - .filter { it.startsWith(partiallyTyped ?: "") } - .take(MAX_RESULTS) - .associateBy { it } + val map = versions() + .filter { it.startsWith(partiallyTyped ?: "") } + .take(MAX_RESULTS) + .associateBy { it } - suggestStringMap(map, FilterStrategy.Contains) - } + suggestStringMap(map, FilterStrategy.Contains) + } } /** @@ -43,11 +43,11 @@ inline fun ConverterBuilder.autocompleteVersions( */ @Suppress("TooGenericExceptionCaught") // sorry, but that's what Linkie throws fun String.toNamespace(): Namespace = when (this) { - "hashed", "hashed_mojang", "hashed-mojang" -> MojangHashedNamespace - "quilt-mappings", "quilt" -> QuiltMappingsNamespace - else -> try { - Namespaces[this] - } catch (e: NullPointerException) { - throw DiscordRelayedException("Invalid namespace: $this") - } + "hashed", "hashed_mojang", "hashed-mojang" -> MojangHashedNamespace + "quilt-mappings", "quilt" -> QuiltMappingsNamespace + else -> try { + Namespaces[this] + } catch (e: NullPointerException) { + throw DiscordRelayedException("Invalid namespace: $this") + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/Mappings.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/Mappings.kt index fcb839ea1c..c7b7b82a36 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/Mappings.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/Mappings.kt @@ -23,575 +23,575 @@ private typealias Matches = Map /** Given a set of result classes, format them into a list of pages for the paginator. **/ fun classesToPages( - namespace: Namespace, - classes: List + namespace: Namespace, + classes: List, ): List> { - val pages = mutableListOf>() - - classes.chunked(PAGE_SIZE).forEach { result -> - val shortPage = result.joinToString("\n\n") { clazz -> - buildString { - append("**Class:** `${clazz.optimumName}`\n") - - val (clientName, serverName) = clazz.obfName.stringPairs() - - if (clientName != null && clientName.isNotEmpty()) { - if (serverName == null) { - append("**Name:** `$clientName` -> ") - } else { - append("**Client:** `$clientName` -> ") - } - - append( - "`${clazz.intermediaryName}`" + - ( - clazz.mappedName.mapIfNotNullOrNotEquals(clazz.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - if (serverName != null && serverName.isNotEmpty()) { - if (clientName != null) { - append("\n") - } - - append("**Server:** `$serverName` -> ") - - append( - "`${clazz.intermediaryName}`" + - ( - clazz.mappedName.mapIfNotNullOrNotEquals(clazz.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - }.trimEnd('\n') - } - - val longPage = result.joinToString("\n\n") { clazz -> - buildString { - append("**Class:** `${clazz.optimumName}`\n") - - val (clientName, serverName) = clazz.obfName.stringPairs() - - if (clientName != null && clientName.isNotEmpty()) { - if (serverName == null) { - append("**Name:** `$clientName` -> ") - } else { - append("**Client:** `$clientName` -> ") - } - - append( - "`${clazz.intermediaryName}`" + - ( - clazz.mappedName.mapIfNotNullOrNotEquals(clazz.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - if (serverName != null && serverName.isNotEmpty()) { - if (clientName != null) { - append("\n") - } - - append("**Server:** `$serverName` -> ") - - append( - "`${clazz.intermediaryName}`" + - ( - clazz.mappedName.mapIfNotNullOrNotEquals(clazz.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - append("\n") - - if (namespace.supportsAT()) { - append( - "**Access Transformer:** `public " + - clazz.intermediaryName.replace('/', '.') + - "`" - ) - } else if (namespace.supportsAW()) { - append( - "\n**Access Widener:** `accessible class ${clazz.optimumName}`" - ) - } - }.trimEnd('\n') - } - - pages.add(Pair(shortPage, longPage)) - } - - return pages + val pages = mutableListOf>() + + classes.chunked(PAGE_SIZE).forEach { result -> + val shortPage = result.joinToString("\n\n") { clazz -> + buildString { + append("**Class:** `${clazz.optimumName}`\n") + + val (clientName, serverName) = clazz.obfName.stringPairs() + + if (clientName != null && clientName.isNotEmpty()) { + if (serverName == null) { + append("**Name:** `$clientName` -> ") + } else { + append("**Client:** `$clientName` -> ") + } + + append( + "`${clazz.intermediaryName}`" + + ( + clazz.mappedName.mapIfNotNullOrNotEquals(clazz.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + if (serverName != null && serverName.isNotEmpty()) { + if (clientName != null) { + append("\n") + } + + append("**Server:** `$serverName` -> ") + + append( + "`${clazz.intermediaryName}`" + + ( + clazz.mappedName.mapIfNotNullOrNotEquals(clazz.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + }.trimEnd('\n') + } + + val longPage = result.joinToString("\n\n") { clazz -> + buildString { + append("**Class:** `${clazz.optimumName}`\n") + + val (clientName, serverName) = clazz.obfName.stringPairs() + + if (clientName != null && clientName.isNotEmpty()) { + if (serverName == null) { + append("**Name:** `$clientName` -> ") + } else { + append("**Client:** `$clientName` -> ") + } + + append( + "`${clazz.intermediaryName}`" + + ( + clazz.mappedName.mapIfNotNullOrNotEquals(clazz.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + if (serverName != null && serverName.isNotEmpty()) { + if (clientName != null) { + append("\n") + } + + append("**Server:** `$serverName` -> ") + + append( + "`${clazz.intermediaryName}`" + + ( + clazz.mappedName.mapIfNotNullOrNotEquals(clazz.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + append("\n") + + if (namespace.supportsAT()) { + append( + "**Access Transformer:** `public " + + clazz.intermediaryName.replace('/', '.') + + "`" + ) + } else if (namespace.supportsAW()) { + append( + "\n**Access Widener:** `accessible class ${clazz.optimumName}`" + ) + } + }.trimEnd('\n') + } + + pages.add(Pair(shortPage, longPage)) + } + + return pages } /** Given a set of result classes, format them into a list of pages for the paginator. **/ fun classesToPages( - namespace: Namespace, - queryResult: ClassResults + namespace: Namespace, + queryResult: ClassResults, ) = - classesToPages(namespace, queryResult.map { it.map { inner -> inner.value }.toList() }.value) + classesToPages(namespace, queryResult.map { it.map { inner -> inner.value }.toList() }.value) /** * A convenience function variable for having an unused MappingsContainer argument. * This is to allow a more general function to be used for all queries. */ val classesToPages = { namespace: Namespace, _: MappingsContainer, classes: ClassResults, _: Boolean -> - classesToPages(namespace, classes) + classesToPages(namespace, classes) } /** Given a set of result fields, format them into a list of pages for the paginator. **/ fun fieldsToPages( - namespace: Namespace, - mappings: MappingsContainer, - fields: List>, - mapDescriptors: Boolean = true + namespace: Namespace, + mappings: MappingsContainer, + fields: List>, + mapDescriptors: Boolean = true, ): List> { - val pages = mutableListOf>() - - fields.chunked(PAGE_SIZE).forEach { result -> - val shortPage = result.joinToString("\n\n") { - val (clazz, field) = it - - val desc = if (mapDescriptors) { - field.getMappedDesc(mappings) - } else { - field.intermediaryDesc - } - - buildString { - append("**Field:** `${clazz.optimumName}::${field.optimumName}`\n") - - val (clientName, serverName) = field.obfName.stringPairs() - - if (!clientName.isNullOrEmpty()) { - if (!serverName.isNullOrEmpty()) { - append("**Name:** `$clientName` -> ") - } else { - append("**Client:** `$clientName` -> ") - } - - append( - "`${field.intermediaryName}`" + - ( - field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - if (!serverName.isNullOrEmpty()) { - if (!clientName.isNullOrEmpty()) { - append("\n") - } - - append("**Server:** `$serverName` -> ") - - append( - "`${field.intermediaryName}`" + - ( - field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - if (namespace.supportsFieldDescription()) { - append("\n") - append("**Types:** `${desc.localiseFieldDesc()}`") - } - } - } - - val longPage = result.joinToString("\n\n") { - val (clazz, field) = it - val mappedDesc = field.getMappedDesc(mappings) - - buildString { - append("**Field:** `${clazz.optimumName}::${field.optimumName}`\n") - - val (clientName, serverName) = field.obfName.stringPairs() - - if (!clientName.isNullOrEmpty()) { - if (!serverName.isNullOrEmpty()) { - append("**Name:** `$clientName` -> ") - } else { - append("**Client:** `$clientName` -> ") - } - - append( - "`${field.intermediaryName}`" + - ( - field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - if (!serverName.isNullOrEmpty()) { - if (!clientName.isNullOrEmpty()) { - append("\n") - } - - append("**Server:** `$serverName` -> ") - - append( - "`${field.intermediaryName}`" + - ( - field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - if (namespace.supportsFieldDescription()) { - append("\n") - append("**Types:** `${mappedDesc.localiseFieldDesc()}`") - } - - append("\n") - - if (namespace.supportsMixin()) { - append("\n") - - append( - "**Mixin Target:** `" + - "L${clazz.optimumName};" + - field.optimumName + - ":" + - mappedDesc + - "`" - ) - } - - if (namespace.supportsAT()) { - append("\n") - - append("**Access Transformer:** `${field.intermediaryName} # ${field.optimumName}`") - } else if (namespace.supportsAW()) { - append("\n") - - append( - "**Access Widener:** `accessible field ${clazz.optimumName} ${field.optimumName} $mappedDesc`" - ) - } - }.trimEnd('\n') - } - - pages.add(Pair(shortPage, longPage)) - } - - return pages + val pages = mutableListOf>() + + fields.chunked(PAGE_SIZE).forEach { result -> + val shortPage = result.joinToString("\n\n") { + val (clazz, field) = it + + val desc = if (mapDescriptors) { + field.getMappedDesc(mappings) + } else { + field.intermediaryDesc + } + + buildString { + append("**Field:** `${clazz.optimumName}::${field.optimumName}`\n") + + val (clientName, serverName) = field.obfName.stringPairs() + + if (!clientName.isNullOrEmpty()) { + if (!serverName.isNullOrEmpty()) { + append("**Name:** `$clientName` -> ") + } else { + append("**Client:** `$clientName` -> ") + } + + append( + "`${field.intermediaryName}`" + + ( + field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + if (!serverName.isNullOrEmpty()) { + if (!clientName.isNullOrEmpty()) { + append("\n") + } + + append("**Server:** `$serverName` -> ") + + append( + "`${field.intermediaryName}`" + + ( + field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + if (namespace.supportsFieldDescription()) { + append("\n") + append("**Types:** `${desc.localiseFieldDesc()}`") + } + } + } + + val longPage = result.joinToString("\n\n") { + val (clazz, field) = it + val mappedDesc = field.getMappedDesc(mappings) + + buildString { + append("**Field:** `${clazz.optimumName}::${field.optimumName}`\n") + + val (clientName, serverName) = field.obfName.stringPairs() + + if (!clientName.isNullOrEmpty()) { + if (!serverName.isNullOrEmpty()) { + append("**Name:** `$clientName` -> ") + } else { + append("**Client:** `$clientName` -> ") + } + + append( + "`${field.intermediaryName}`" + + ( + field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + if (!serverName.isNullOrEmpty()) { + if (!clientName.isNullOrEmpty()) { + append("\n") + } + + append("**Server:** `$serverName` -> ") + + append( + "`${field.intermediaryName}`" + + ( + field.mappedName.mapIfNotNullOrNotEquals(field.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + if (namespace.supportsFieldDescription()) { + append("\n") + append("**Types:** `${mappedDesc.localiseFieldDesc()}`") + } + + append("\n") + + if (namespace.supportsMixin()) { + append("\n") + + append( + "**Mixin Target:** `" + + "L${clazz.optimumName};" + + field.optimumName + + ":" + + mappedDesc + + "`" + ) + } + + if (namespace.supportsAT()) { + append("\n") + + append("**Access Transformer:** `${field.intermediaryName} # ${field.optimumName}`") + } else if (namespace.supportsAW()) { + append("\n") + + append( + "**Access Widener:** `accessible field ${clazz.optimumName} ${field.optimumName} $mappedDesc`" + ) + } + }.trimEnd('\n') + } + + pages.add(Pair(shortPage, longPage)) + } + + return pages } /** Given a set of result fields, format them into a list of pages for the paginator. **/ fun fieldsToPages( - namespace: Namespace, - mappings: MappingsContainer, - queryResult: QueryResult, - mapDescriptors: Boolean = true + namespace: Namespace, + mappings: MappingsContainer, + queryResult: QueryResult, + mapDescriptors: Boolean = true, ) = - fieldsToPages( - namespace, - mappings, - queryResult.map { it.map { inner -> inner.value }.toList() }.value, - mapDescriptors - ) + fieldsToPages( + namespace, + mappings, + queryResult.map { it.map { inner -> inner.value }.toList() }.value, + mapDescriptors + ) /** Given a set of result methods, format them into a list of pages for the paginator. **/ fun methodsToPages( - namespace: Namespace, - mappings: MappingsContainer, - methods: List>, - mapDescriptors: Boolean = true + namespace: Namespace, + mappings: MappingsContainer, + methods: List>, + mapDescriptors: Boolean = true, ): List> { - val pages = mutableListOf>() - - methods.chunked(PAGE_SIZE).forEach { result -> - val shortPage = result.joinToString("\n\n") { - val (clazz, method) = it - buildString { - append("**Method:** `${clazz.optimumName}::${method.optimumName}`\n") - - val (clientName, serverName) = method.obfName.stringPairs() - - if (!clientName.isNullOrEmpty()) { - if (!serverName.isNullOrEmpty()) { - append("**Name:** `$clientName` -> ") - } else { - append("**Client:** `$clientName` -> ") - } - - append( - "`${method.intermediaryName}`" + - ( - method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - if (!serverName.isNullOrEmpty()) { - if (!clientName.isNullOrEmpty()) { - append("\n") - } - - append("**Server:** `$serverName` -> ") - - append( - "`${method.intermediaryName}`" + - ( - method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - }.trimEnd('\n') - } - - val longPage = result.joinToString("\n\n") { - val (clazz, method) = it - val desc = if (mapDescriptors) { - method.getMappedDesc(mappings) - } else { - method.intermediaryDesc - } - - buildString { - append("**Method:** `${clazz.optimumName}::${method.optimumName}`\n") - - val (clientName, serverName) = method.obfName.stringPairs() - - if (clientName != null && clientName.isNotEmpty()) { - if (serverName == null) { - append("**Name:** `$clientName` -> ") - } else { - append("**Client:** `$clientName` -> ") - } - - append( - "`${method.intermediaryName}`" + - ( - method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - if (serverName != null && serverName.isNotEmpty()) { - if (clientName != null) { - append("\n") - } - - append("**Server:** `$serverName` -> ") - - append( - "`${method.intermediaryName}`" + - ( - method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { name -> - " -> `$name`" - } ?: "" - ) - ) - } - - append("\n") - - if (namespace.supportsMixin()) { - append("\n") - - append( - "**Mixin Target** `" + - "L${clazz.optimumName};" + - method.optimumName + - desc + - "`" - ) - } - - if (namespace.supportsAT()) { - append("\n") - - append( - "**Access Transformer** `public" + clazz.optimumName.replace('/', '.') + - method.intermediaryName + - desc + - " # ${method.optimumName}`" - ) - } else if (namespace.supportsAW()) { - append("\n") - - append("**Access Widener** `accessible method ${clazz.optimumName} ${method.optimumName} $desc`") - } - }.trimEnd('\n') - } - - pages.add(Pair(shortPage, longPage)) - } - - return pages + val pages = mutableListOf>() + + methods.chunked(PAGE_SIZE).forEach { result -> + val shortPage = result.joinToString("\n\n") { + val (clazz, method) = it + buildString { + append("**Method:** `${clazz.optimumName}::${method.optimumName}`\n") + + val (clientName, serverName) = method.obfName.stringPairs() + + if (!clientName.isNullOrEmpty()) { + if (!serverName.isNullOrEmpty()) { + append("**Name:** `$clientName` -> ") + } else { + append("**Client:** `$clientName` -> ") + } + + append( + "`${method.intermediaryName}`" + + ( + method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + if (!serverName.isNullOrEmpty()) { + if (!clientName.isNullOrEmpty()) { + append("\n") + } + + append("**Server:** `$serverName` -> ") + + append( + "`${method.intermediaryName}`" + + ( + method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + }.trimEnd('\n') + } + + val longPage = result.joinToString("\n\n") { + val (clazz, method) = it + val desc = if (mapDescriptors) { + method.getMappedDesc(mappings) + } else { + method.intermediaryDesc + } + + buildString { + append("**Method:** `${clazz.optimumName}::${method.optimumName}`\n") + + val (clientName, serverName) = method.obfName.stringPairs() + + if (clientName != null && clientName.isNotEmpty()) { + if (serverName == null) { + append("**Name:** `$clientName` -> ") + } else { + append("**Client:** `$clientName` -> ") + } + + append( + "`${method.intermediaryName}`" + + ( + method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + if (serverName != null && serverName.isNotEmpty()) { + if (clientName != null) { + append("\n") + } + + append("**Server:** `$serverName` -> ") + + append( + "`${method.intermediaryName}`" + + ( + method.mappedName.mapIfNotNullOrNotEquals(method.intermediaryName) { name -> + " -> `$name`" + } ?: "" + ) + ) + } + + append("\n") + + if (namespace.supportsMixin()) { + append("\n") + + append( + "**Mixin Target** `" + + "L${clazz.optimumName};" + + method.optimumName + + desc + + "`" + ) + } + + if (namespace.supportsAT()) { + append("\n") + + append( + "**Access Transformer** `public" + clazz.optimumName.replace('/', '.') + + method.intermediaryName + + desc + + " # ${method.optimumName}`" + ) + } else if (namespace.supportsAW()) { + append("\n") + + append("**Access Widener** `accessible method ${clazz.optimumName} ${method.optimumName} $desc`") + } + }.trimEnd('\n') + } + + pages.add(Pair(shortPage, longPage)) + } + + return pages } /** Given a set of result methods, format them into a list of pages for the paginator. **/ fun methodsToPages( - namespace: Namespace, - mappings: MappingsContainer, - queryResult: QueryResult, - mapDescriptors: Boolean = true + namespace: Namespace, + mappings: MappingsContainer, + queryResult: QueryResult, + mapDescriptors: Boolean = true, ) = methodsToPages( - namespace, - mappings, - queryResult.map { it.map { inner -> inner.value }.toList() }.value, - mapDescriptors + namespace, + mappings, + queryResult.map { it.map { inner -> inner.value }.toList() }.value, + mapDescriptors ) /** Given a set of class mapping matches, format them into a list of pages for the paginator. **/ fun classMatchesToPages( - outputContainer: MappingsContainer, - matches: List> + outputContainer: MappingsContainer, + matches: List>, ): List { - val pages = mutableListOf() + val pages = mutableListOf() - for (match in matches.chunked(PAGE_SIZE)) { - val text = match.joinToString("\n\n") { (input, output) -> + for (match in matches.chunked(PAGE_SIZE)) { + val text = match.joinToString("\n\n") { (input, output) -> - val inputName = input.mappedName ?: input.optimumName + val inputName = input.mappedName ?: input.optimumName - val outputName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { - // Because of requests and how hashed is often used for its hashes rather than the names, - // we call from "intermediary" instead of mapped. - output.intermediaryName - } else { - output.mappedName ?: output.optimumName - } + val outputName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { + // Because of requests and how hashed is often used for its hashes rather than the names, + // we call from "intermediary" instead of mapped. + output.intermediaryName + } else { + output.mappedName ?: output.optimumName + } - "**Class:** `$inputName` -> `$outputName`" - } - pages.add(text) - } + "**Class:** `$inputName` -> `$outputName`" + } + pages.add(text) + } - return pages + return pages } /** Convienence function for making code more generalized. */ val classMatchesToPages = { outputContainer: MappingsContainer, classMatches: Matches -> - classMatchesToPages(outputContainer, classMatches.toList()) + classMatchesToPages(outputContainer, classMatches.toList()) } /** Given a set of field mapping matches, format them into a list of pages for the paginator. **/ fun fieldMatchesToPages( - outputContainer: MappingsContainer, - matches: List, MemberEntry>> + outputContainer: MappingsContainer, + matches: List, MemberEntry>>, ): List { - val pages = mutableListOf() - - for (match in matches.chunked(PAGE_SIZE)) { - val page = match.joinToString("\n\n") { - val (inputClass, inputField) = it.first - val (outputClass, outputField) = it.second - val mappedDesc = outputField.getMappedDesc(outputContainer) - - val inputName = inputField.mappedName ?: inputField.optimumName - - val outputName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { - outputField.intermediaryName - } else { - outputField.mappedName ?: outputField.optimumName - } - - val inputClassName = inputClass.mappedName ?: inputClass.optimumName - - val outputClassName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { - outputClass.intermediaryName - } else { - outputClass.mappedName ?: outputClass.optimumName - } - - buildString { - append("**Field:** `$inputClassName::$inputName` -> `$outputClassName::$outputName`") - - val namespace = if (outputContainer.namespace == "hashed-mojmap") { - // thanks linkie you're ruining everything - MojangHashedNamespace - } else { - Namespaces[outputContainer.namespace] - } - - if (namespace.supportsFieldDescription()) { - append("\n") - append("**Types:** `${mappedDesc.localiseFieldDesc()}`") - } - } - } - pages.add(page) - } - - return pages + val pages = mutableListOf() + + for (match in matches.chunked(PAGE_SIZE)) { + val page = match.joinToString("\n\n") { + val (inputClass, inputField) = it.first + val (outputClass, outputField) = it.second + val mappedDesc = outputField.getMappedDesc(outputContainer) + + val inputName = inputField.mappedName ?: inputField.optimumName + + val outputName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { + outputField.intermediaryName + } else { + outputField.mappedName ?: outputField.optimumName + } + + val inputClassName = inputClass.mappedName ?: inputClass.optimumName + + val outputClassName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { + outputClass.intermediaryName + } else { + outputClass.mappedName ?: outputClass.optimumName + } + + buildString { + append("**Field:** `$inputClassName::$inputName` -> `$outputClassName::$outputName`") + + val namespace = if (outputContainer.namespace == "hashed-mojmap") { + // thanks linkie you're ruining everything + MojangHashedNamespace + } else { + Namespaces[outputContainer.namespace] + } + + if (namespace.supportsFieldDescription()) { + append("\n") + append("**Types:** `${mappedDesc.localiseFieldDesc()}`") + } + } + } + pages.add(page) + } + + return pages } /** Convenience function for making code more generalized. */ val fieldMatchesToPages = { outputContainer: MappingsContainer, fieldMatches: Matches> -> - fieldMatchesToPages(outputContainer, fieldMatches.toList()) + fieldMatchesToPages(outputContainer, fieldMatches.toList()) } /** Given a set of method mapping matches, format them into a list of pages for the paginator. **/ fun methodMatchesToPages( - outputContainer: MappingsContainer, - matches: List, MemberEntry>> + outputContainer: MappingsContainer, + matches: List, MemberEntry>>, ): List { - val pages = mutableListOf() + val pages = mutableListOf() - for (match in matches.chunked(PAGE_SIZE)) { - val page = match.joinToString("\n\n") { - val (inputClass, inputMethod) = it.first - val (outputClass, outputMethod) = it.second - val mappedDesc = outputMethod.getMappedDesc(outputContainer) + for (match in matches.chunked(PAGE_SIZE)) { + val page = match.joinToString("\n\n") { + val (inputClass, inputMethod) = it.first + val (outputClass, outputMethod) = it.second + val mappedDesc = outputMethod.getMappedDesc(outputContainer) - val inputName = inputMethod.mappedName ?: inputMethod.optimumName + val inputName = inputMethod.mappedName ?: inputMethod.optimumName - val outputName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { - outputMethod.intermediaryName - } else { - outputMethod.mappedName ?: outputMethod.optimumName - } + val outputName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { + outputMethod.intermediaryName + } else { + outputMethod.mappedName ?: outputMethod.optimumName + } - val inputClassName = inputClass.mappedName ?: inputClass.optimumName + val inputClassName = inputClass.mappedName ?: inputClass.optimumName - val outputClassName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { - outputClass.intermediaryName - } else { - outputClass.mappedName ?: outputClass.optimumName - } + val outputClassName = if (outputContainer.namespace.toNamespace() == MojangHashedNamespace) { + outputClass.intermediaryName + } else { + outputClass.mappedName ?: outputClass.optimumName + } - "**Method:** `$inputClassName::$inputName` -> `$outputClassName::$outputName`" + - "\n**Description:** `$mappedDesc`" - } - pages.add(page) - } + "**Method:** `$inputClassName::$inputName` -> `$outputClassName::$outputName`" + + "\n**Description:** `$mappedDesc`" + } + pages.add(page) + } - return pages + return pages } /** Convienence function for making code more generalized. */ val methodMatchesToPages = { outputContainer: MappingsContainer, methodMatches: Matches> -> - methodMatchesToPages(outputContainer, methodMatches.toList()) + methodMatchesToPages(outputContainer, methodMatches.toList()) } /** Attempt to get an obfuscated name from three possible states. */ val Obf.preferredName: String? - get() = merged ?: client ?: server + get() = merged ?: client ?: server diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/McpNamespaceReplacement.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/McpNamespaceReplacement.kt index 06a8836e37..e26f899b4e 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/McpNamespaceReplacement.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/McpNamespaceReplacement.kt @@ -28,159 +28,159 @@ import java.net.URL * being down indefinitely. */ object McpNamespaceReplacement : Namespace("mcp") { - private const val forgeMaven = "http://maven.minecraftforge.net/de/oceanlabs/mcp" - private const val mcpArchive = "https://raw.githubusercontent.com/ModCoderPack/MCPMappingsArchive/master" - private val mcpConfigSnapshots = mutableMapOf>() - private val newMcpVersions = mutableMapOf() - - init { - buildSupplier { - cached() - - buildVersions { - versionsSeq(::getAllBotVersions) - uuid { "$it-${mcpConfigSnapshots[it.toVersion()]?.maxOrNull()!!}" } - - buildMappings(name = "MCP") { - val latestSnapshot = mcpConfigSnapshots[it.toVersion()]?.maxOrNull()!! - @Suppress("MagicNumber") // we're constructing a version, we need 13 - source( - if (it.toVersion() >= Version(1, 13)) { - loadTsrgFromURLZip(URL("$forgeMaven/mcp_config/$it/mcp_config-$it.zip")) - MappingsSource.MCP_TSRG - } else { - val link = "$forgeMaven/mcp/$it/mcp-$it-srg.zip" - loadSrgFromURLZip(URL(link)) - MappingsSource.MCP_SRG - } - ) - val link = "$mcpArchive/mcp_snapshot/$latestSnapshot-$it/mcp_snapshot-$latestSnapshot-$it.zip" - loadMCPFromURLZip(URL(link)) - } - } - - buildVersions { - versions { newMcpVersions.keys.map(Version::toString) } - uuid { newMcpVersions[it.toVersion()]!!.name } - - buildMappings(name = "MCP") { - val mcpVersion = newMcpVersions[it.toVersion()]!! - loadTsrgFromURLZip(URL(mcpVersion.mcp_config)) - loadMCPFromURLZip(URL(mcpVersion.mcp)) - source(MappingsSource.MCP_TSRG) - } - } - } - } - - /** @suppress **/ - fun getAllBotVersions(): Sequence = mcpConfigSnapshots.keys.asSequence().map { it.toString() } - - override fun supportsFieldDescription(): Boolean = false - override fun getDefaultLoadedVersions(): List = listOf(defaultVersion) - override fun getAllVersions(): Sequence = getAllBotVersions() + newMcpVersions.keys.map(Version::toString) - - override fun supportsAT(): Boolean = true - override fun supportsMixin(): Boolean = true - override suspend fun reloadData() { - mcpConfigSnapshots.clear() - - val versionsJson = URL("$mcpArchive/versions.json").readText() - - json.parseToJsonElement(versionsJson).jsonObject.forEach { mcVersion, mcpVersionsObj -> - val list = mcpConfigSnapshots.getOrPut(mcVersion.toVersion()) { mutableListOf() } - mcpVersionsObj.jsonObject["snapshot"]?.jsonArray?.forEach { - list.add(it.jsonPrimitive.content) - } - } - mcpConfigSnapshots.filterValues { it.isEmpty() }.keys.toMutableList().forEach { - mcpConfigSnapshots.remove(it) - } - - newMcpVersions.clear() - val tmpMcpVersionsJson = json.decodeFromString( - MapSerializer(String.serializer(), MCPVersion.serializer()), - URL(MCPNamespace.tmpMcpVersionsUrl).readText() - ) - - tmpMcpVersionsJson.forEach { (mcVersion, mcpVersion) -> - newMcpVersions[mcVersion.toVersion()] = mcpVersion - } - } - - /** @suppress **/ - suspend fun MappingsBuilder.loadTsrgFromURLZip(url: URL) { - url.toAsyncZip().forEachEntry { path, entry -> - if (!entry.isDirectory && path.split("/").lastOrNull() == "joined.tsrg") { - loadTsrgFromInputStream(entry.bytes.decodeToString()) - } - } - } - - /** @suppress **/ - suspend fun MappingsBuilder.loadSrgFromURLZip(url: URL) { - url.toAsyncZip().forEachEntry { path, entry -> - if (!entry.isDirectory && path.split("/").lastOrNull() == "joined.srg") { - loadSrgFromInputStream(entry.bytes.decodeToString()) - } - } - } - - private fun MappingsBuilder.loadSrgFromInputStream(content: String) { - apply( - srg(content), - obfMerged = "obf", - intermediary = "srg", - ) - } - - private fun MappingsBuilder.loadTsrgFromInputStream(content: String) { - apply( - tsrg(content), - obfMerged = "obf", - intermediary = "srg", - ) - } - - /** @suppress **/ - suspend fun MappingsBuilder.loadMCPFromURLZip(url: URL) { - url.toAsyncZip().forEachEntry { path, entry -> - if (!entry.isDirectory) { - when (path.split("/").lastOrNull()) { - "fields.csv" -> loadMCPFieldsCSVFromInputStream(entry.bytes.lines()) - "methods.csv" -> loadMCPMethodsCSVFromInputStream(entry.bytes.lines()) - } - } - } - } - - private fun MappingsBuilder.loadMCPFieldsCSVFromInputStream(lines: Sequence) { - val map = mutableMapOf() - lines.filterNotBlank().forEach { - val split = it.split(',') - map[split[0]] = split[1] - } - container.classes.forEach { (_, it) -> - it.fields.forEach { field -> - map[field.intermediaryName]?.apply { - field.mappedName = this - } - } - } - } - - private fun MappingsBuilder.loadMCPMethodsCSVFromInputStream(lines: Sequence) { - val map = mutableMapOf() - lines.filterNotBlank().forEach { - val split = it.split(',') - map[split[0]] = split[1] - } - container.classes.forEach { (_, it) -> - it.methods.forEach { method -> - map[method.intermediaryName]?.apply { - method.mappedName = this - } - } - } - } + private const val forgeMaven = "http://maven.minecraftforge.net/de/oceanlabs/mcp" + private const val mcpArchive = "https://raw.githubusercontent.com/ModCoderPack/MCPMappingsArchive/master" + private val mcpConfigSnapshots = mutableMapOf>() + private val newMcpVersions = mutableMapOf() + + init { + buildSupplier { + cached() + + buildVersions { + versionsSeq(::getAllBotVersions) + uuid { "$it-${mcpConfigSnapshots[it.toVersion()]?.maxOrNull()!!}" } + + buildMappings(name = "MCP") { + val latestSnapshot = mcpConfigSnapshots[it.toVersion()]?.maxOrNull()!! + @Suppress("MagicNumber") // we're constructing a version, we need 13 + source( + if (it.toVersion() >= Version(1, 13)) { + loadTsrgFromURLZip(URL("$forgeMaven/mcp_config/$it/mcp_config-$it.zip")) + MappingsSource.MCP_TSRG + } else { + val link = "$forgeMaven/mcp/$it/mcp-$it-srg.zip" + loadSrgFromURLZip(URL(link)) + MappingsSource.MCP_SRG + } + ) + val link = "$mcpArchive/mcp_snapshot/$latestSnapshot-$it/mcp_snapshot-$latestSnapshot-$it.zip" + loadMCPFromURLZip(URL(link)) + } + } + + buildVersions { + versions { newMcpVersions.keys.map(Version::toString) } + uuid { newMcpVersions[it.toVersion()]!!.name } + + buildMappings(name = "MCP") { + val mcpVersion = newMcpVersions[it.toVersion()]!! + loadTsrgFromURLZip(URL(mcpVersion.mcp_config)) + loadMCPFromURLZip(URL(mcpVersion.mcp)) + source(MappingsSource.MCP_TSRG) + } + } + } + } + + /** @suppress **/ + fun getAllBotVersions(): Sequence = mcpConfigSnapshots.keys.asSequence().map { it.toString() } + + override fun supportsFieldDescription(): Boolean = false + override fun getDefaultLoadedVersions(): List = listOf(defaultVersion) + override fun getAllVersions(): Sequence = getAllBotVersions() + newMcpVersions.keys.map(Version::toString) + + override fun supportsAT(): Boolean = true + override fun supportsMixin(): Boolean = true + override suspend fun reloadData() { + mcpConfigSnapshots.clear() + + val versionsJson = URL("$mcpArchive/versions.json").readText() + + json.parseToJsonElement(versionsJson).jsonObject.forEach { mcVersion, mcpVersionsObj -> + val list = mcpConfigSnapshots.getOrPut(mcVersion.toVersion()) { mutableListOf() } + mcpVersionsObj.jsonObject["snapshot"]?.jsonArray?.forEach { + list.add(it.jsonPrimitive.content) + } + } + mcpConfigSnapshots.filterValues { it.isEmpty() }.keys.toMutableList().forEach { + mcpConfigSnapshots.remove(it) + } + + newMcpVersions.clear() + val tmpMcpVersionsJson = json.decodeFromString( + MapSerializer(String.serializer(), MCPVersion.serializer()), + URL(MCPNamespace.tmpMcpVersionsUrl).readText() + ) + + tmpMcpVersionsJson.forEach { (mcVersion, mcpVersion) -> + newMcpVersions[mcVersion.toVersion()] = mcpVersion + } + } + + /** @suppress **/ + suspend fun MappingsBuilder.loadTsrgFromURLZip(url: URL) { + url.toAsyncZip().forEachEntry { path, entry -> + if (!entry.isDirectory && path.split("/").lastOrNull() == "joined.tsrg") { + loadTsrgFromInputStream(entry.bytes.decodeToString()) + } + } + } + + /** @suppress **/ + suspend fun MappingsBuilder.loadSrgFromURLZip(url: URL) { + url.toAsyncZip().forEachEntry { path, entry -> + if (!entry.isDirectory && path.split("/").lastOrNull() == "joined.srg") { + loadSrgFromInputStream(entry.bytes.decodeToString()) + } + } + } + + private fun MappingsBuilder.loadSrgFromInputStream(content: String) { + apply( + srg(content), + obfMerged = "obf", + intermediary = "srg", + ) + } + + private fun MappingsBuilder.loadTsrgFromInputStream(content: String) { + apply( + tsrg(content), + obfMerged = "obf", + intermediary = "srg", + ) + } + + /** @suppress **/ + suspend fun MappingsBuilder.loadMCPFromURLZip(url: URL) { + url.toAsyncZip().forEachEntry { path, entry -> + if (!entry.isDirectory) { + when (path.split("/").lastOrNull()) { + "fields.csv" -> loadMCPFieldsCSVFromInputStream(entry.bytes.lines()) + "methods.csv" -> loadMCPMethodsCSVFromInputStream(entry.bytes.lines()) + } + } + } + } + + private fun MappingsBuilder.loadMCPFieldsCSVFromInputStream(lines: Sequence) { + val map = mutableMapOf() + lines.filterNotBlank().forEach { + val split = it.split(',') + map[split[0]] = split[1] + } + container.classes.forEach { (_, it) -> + it.fields.forEach { field -> + map[field.intermediaryName]?.apply { + field.mappedName = this + } + } + } + } + + private fun MappingsBuilder.loadMCPMethodsCSVFromInputStream(lines: Sequence) { + val map = mutableMapOf() + lines.filterNotBlank().forEach { + val split = it.split(',') + map[split[0]] = split[1] + } + container.classes.forEach { (_, it) -> + it.methods.forEach { method -> + map[method.intermediaryName]?.apply { + method.mappedName = this + } + } + } + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/MojangReleaseContainer.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/MojangReleaseContainer.kt index 62483e79a6..4ce5b1ea8d 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/MojangReleaseContainer.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/MojangReleaseContainer.kt @@ -13,31 +13,31 @@ import kotlin.reflect.jvm.isAccessible /** Mojang release container, allowing for retrieval of various Mojang mappings release versions. **/ object MojangReleaseContainer { - /** - * A wrapper for [MojangNamespace.latestRelease], a private field. - */ - var latestRelease: String - get() = latestReleaseProperty.getter.call() - set(value) = latestReleaseProperty.setter.call(value) + /** + * A wrapper for [MojangNamespace.latestRelease], a private field. + */ + var latestRelease: String + get() = latestReleaseProperty.getter.call() + set(value) = latestReleaseProperty.setter.call(value) - /** - * A wrapper for [MojangNamespace.latestSnapshot], a private field. - */ - var latestSnapshot: String - get() = latestSnapshotProperty.getter.call() - set(value) = latestSnapshotProperty.setter.call(value) + /** + * A wrapper for [MojangNamespace.latestSnapshot], a private field. + */ + var latestSnapshot: String + get() = latestSnapshotProperty.getter.call() + set(value) = latestSnapshotProperty.setter.call(value) - @Suppress("UNCHECKED_CAST") - private val latestSnapshotProperty by lazy { - MojangNamespace::class.declaredMemberProperties - .first { it.name == "latestSnapshot" } - .also { it.isAccessible = true } as KMutableProperty1 - } + @Suppress("UNCHECKED_CAST") + private val latestSnapshotProperty by lazy { + MojangNamespace::class.declaredMemberProperties + .first { it.name == "latestSnapshot" } + .also { it.isAccessible = true } as KMutableProperty1 + } - @Suppress("UNCHECKED_CAST") - private val latestReleaseProperty by lazy { - MojangNamespace::class.declaredMemberProperties - .first { it.name == "latestRelease" } - .also { it.isAccessible = true } as KMutableProperty1 - } + @Suppress("UNCHECKED_CAST") + private val latestReleaseProperty by lazy { + MojangNamespace::class.declaredMemberProperties + .first { it.name == "latestRelease" } + .also { it.isAccessible = true } as KMutableProperty1 + } } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/YarnReleaseContainer.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/YarnReleaseContainer.kt index 23fdcfc9d7..a5fb2202df 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/YarnReleaseContainer.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/YarnReleaseContainer.kt @@ -17,16 +17,16 @@ import me.shedaniel.linkie.utils.tryToVersion * for different "channels". */ object YarnReleaseContainer { - /** - * The latest Minecraft release to have yarn mappings built for it. - */ - val latestRelease = YarnNamespace.latestYarnVersion + /** + * The latest Minecraft release to have yarn mappings built for it. + */ + val latestRelease = YarnNamespace.latestYarnVersion - /** - * The latest Minecraft snapshot release to have yarn mappings built for it. - * This is generated in a similar fashion to [YarnNamespace.latestYarnVersion] - * since Linkie removed channel support. - */ - val latestSnapshot = YarnNamespace.yarnBuilds.keys - .mapNotNull { it.tryToVersion() }.maxOrNull()?.toString() + /** + * The latest Minecraft snapshot release to have yarn mappings built for it. + * This is generated in a similar fashion to [YarnNamespace.latestYarnVersion] + * since Linkie removed channel support. + */ + val latestSnapshot = YarnNamespace.yarnBuilds.keys + .mapNotNull { it.tryToVersion() }.maxOrNull()?.toString() } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/linkie/Functions.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/linkie/Functions.kt index fd8226f5e0..3044174737 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/linkie/Functions.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/utils/linkie/Functions.kt @@ -10,18 +10,18 @@ import me.shedaniel.linkie.Obf /** Format this obfuscated member as a pair of strings, client to server. **/ fun Obf.stringPairs(): Pair = when { - isEmpty() -> "" to null - isMerged() -> merged!! to null + isEmpty() -> "" to null + isMerged() -> merged!! to null - else -> client to server + else -> client to server } /** * If not null or equal to the given string, return the string with the given mapping lambda applied, otherwise null. */ inline fun String?.mapIfNotNullOrNotEquals(other: String, mapper: (String) -> String): String? = - when { - isNullOrEmpty() -> null - this == other -> null - else -> mapper(this) - } + when { + isNullOrEmpty() -> null + this == other -> null + else -> mapper(this) + } diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/extra/MappingsPlugin.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/extra/MappingsPlugin.kt index 7b7db50603..5ffbe4d3c6 100644 --- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/extra/MappingsPlugin.kt +++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/extra/MappingsPlugin.kt @@ -14,20 +14,20 @@ import org.pf4j.PluginWrapper /** Class representing a simplified mappings plugin. No configuration supported yet. **/ @WiredPlugin( - MappingsPlugin.PLUGIN_ID, - "0.0.1", - "kord-extensions", - "KordEx Minecraft Mappings plugin", - "MPL 2.0" + MappingsPlugin.PLUGIN_ID, + "0.0.1", + "kord-extensions", + "KordEx Minecraft Mappings plugin", + "MPL 2.0" ) class MappingsPlugin(wrapper: PluginWrapper) : KordExPlugin(wrapper) { - override suspend fun setup() { - MappingsExtension.configure(ExtMappingsBuilder()) - extension(::MappingsExtension) - } + override suspend fun setup() { + MappingsExtension.configure(ExtMappingsBuilder()) + extension(::MappingsExtension) + } - public companion object { - /** This plugin's ID, provided here for re-use. **/ - public const val PLUGIN_ID: String = "ext-mappings" - } + companion object { + /** This plugin's ID, provided here for re-use. **/ + const val PLUGIN_ID: String = "ext-mappings" + } } diff --git a/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings.properties b/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings.properties index b9efcc232b..89371cbb7d 100644 --- a/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings.properties +++ b/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings.properties @@ -3,21 +3,16 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argument.timeout.name=timeout argument.timeout.description=Set the duration (in seconds) until the paginator buttons are removed - command.mapping.name=mappings command.mapping.description=Manage and view the Mappings extension settings for this server - command.mapping.timeout.name=timeout command.mapping.timeout.description=Set a custom timeout for the buttons on mappings paginators command.mapping.timeout.response.updated=**Timeout updated:** {0} command.mapping.timeout.response.current=**Current timeout:** {0} - command.mapping.namespace.name=namespaces command.mapping.namespace.description=Configure which namespaces can be used in conversion commands - command.mapping.namespace.selectmenu=Select the namespaces for conversion commands command.mapping.namespace.selectmenu.cleared=Namespaces cleared -command.mapping.namespace.selectmenu.updated=**Namespaces updated:** {0} \ No newline at end of file +command.mapping.namespace.selectmenu.updated=**Namespaces updated:** {0} diff --git a/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings_en_GB.properties b/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings_en_GB.properties index b9efcc232b..89371cbb7d 100644 --- a/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings_en_GB.properties +++ b/extra-modules/extra-mappings/src/main/resources/translations/kordex/mappings_en_GB.properties @@ -3,21 +3,16 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argument.timeout.name=timeout argument.timeout.description=Set the duration (in seconds) until the paginator buttons are removed - command.mapping.name=mappings command.mapping.description=Manage and view the Mappings extension settings for this server - command.mapping.timeout.name=timeout command.mapping.timeout.description=Set a custom timeout for the buttons on mappings paginators command.mapping.timeout.response.updated=**Timeout updated:** {0} command.mapping.timeout.response.current=**Current timeout:** {0} - command.mapping.namespace.name=namespaces command.mapping.namespace.description=Configure which namespaces can be used in conversion commands - command.mapping.namespace.selectmenu=Select the namespaces for conversion commands command.mapping.namespace.selectmenu.cleared=Namespaces cleared -command.mapping.namespace.selectmenu.updated=**Namespaces updated:** {0} \ No newline at end of file +command.mapping.namespace.selectmenu.updated=**Namespaces updated:** {0} diff --git a/extra-modules/extra-mappings/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt b/extra-modules/extra-mappings/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt index 67cc2aae54..e52887a394 100644 --- a/extra-modules/extra-mappings/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt +++ b/extra-modules/extra-mappings/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt @@ -13,20 +13,20 @@ import com.kotlindiscord.kord.extensions.utils.env import org.koin.core.logger.Level suspend fun main() { - val bot = ExtensibleBot(env("TOKEN")) { - koinLogLevel = Level.DEBUG + val bot = ExtensibleBot(env("TOKEN")) { + koinLogLevel = Level.DEBUG - chatCommands { - check { isNotBot() } - enabled = true - } + chatCommands { + check { isNotBot() } + enabled = true + } - applicationCommands { - enabled = true - } + applicationCommands { + enabled = true + } - extensions { - extMappings { + extensions { + extMappings { // namespaceCheck { namespace -> // { // if (namespace == YarnNamespace) { @@ -36,9 +36,9 @@ suspend fun main() { // } // } // } - } - } - } + } + } + } - bot.start() + bot.start() } diff --git a/extra-modules/extra-mappings/src/test/resources/junit-platform.properties b/extra-modules/extra-mappings/src/test/resources/junit-platform.properties index 580f511dad..6939a7fb02 100644 --- a/extra-modules/extra-mappings/src/test/resources/junit-platform.properties +++ b/extra-modules/extra-mappings/src/test/resources/junit-platform.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - junit.jupiter.execution.parallel.enabled=true diff --git a/extra-modules/extra-mappings/src/test/resources/logback.groovy b/extra-modules/extra-mappings/src/test/resources/logback.groovy index 76c3dc6373..3c126a7dee 100644 --- a/extra-modules/extra-mappings/src/test/resources/logback.groovy +++ b/extra-modules/extra-mappings/src/test/resources/logback.groovy @@ -11,30 +11,30 @@ def environment = System.getenv().getOrDefault("ENVIRONMENT", "production") def defaultLevel = TRACE if (environment == "spam") { - statusListener(OnConsoleStatusListener) + statusListener(OnConsoleStatusListener) - logger("dev.kord.rest.DefaultGateway", TRACE) + logger("dev.kord.rest.DefaultGateway", TRACE) } else { - // Silence warning about missing native PRNG - logger("io.ktor.util.random", ERROR) + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) } appender("CONSOLE", ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" - withJansi = true - } + withJansi = true + } - target = ConsoleTarget.SystemOut + target = ConsoleTarget.SystemOut } appender("FILE", FileAppender) { - file = "output.log" + file = "output.log" - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } } root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/extra-modules/extra-phishing/README.md b/extra-modules/extra-phishing/README.md index 98c98104d2..bdf2577c32 100644 --- a/extra-modules/extra-phishing/README.md +++ b/extra-modules/extra-phishing/README.md @@ -3,11 +3,13 @@ [![Discord: Click here](https://img.shields.io/static/v1?label=Discord&message=Click%20here&color=7289DA&style=for-the-badge&logo=discord)](https://discord.gg/ZKRetPNtvY)
![Latest](https://img.shields.io/maven-metadata/v?label=Latest&metadataUrl=https%3A%2F%2Fs01.oss.sonatype.org%2Fservice%2Flocal%2Frepositories%2Fsnapshots%2Fcontent%2Fcom%2Fkotlindiscord%2Fkord%2Fextensions%2Fkord-extensions%2Fmaven-metadata.xml&style=for-the-badge) -This module contains an extension written to provide some anti-phishing protection, based on the crowdsourced [Sinking Yachts API](https://phish.sinking.yachts/docs). +This module contains an extension written to provide some anti-phishing protection, based on the +crowdsourced [Sinking Yachts API](https://phish.sinking.yachts/docs). # Getting Started -* **Maven repo:** Maven Central for releases, `https://s01.oss.sonatype.org/content/repositories/snapshots` for snapshots +* **Maven repo:** Maven Central for releases, `https://s01.oss.sonatype.org/content/repositories/snapshots` for + snapshots * **Maven coordinates:** `com.kotlindiscord.kord.extensions:extra-phishing:VERSION` At its simplest, you can add this extension directly to your bot with a minimum configuration. For example: @@ -27,7 +29,8 @@ suspend fun main() { } ``` -This will install the extension using its default configuration. However, the extension may be configured in several ways - as is detailed below. +This will install the extension using its default configuration. However, the extension may be configured in several +ways - as is detailed below. # Commands @@ -36,7 +39,8 @@ This extension provides a number of commands for use on Discord. * Slash command: `/phishing-check`, for checking whether the given argument is a phishing domain * Message command: `Phishing Check`, for manually checking a specific message via the right-click menu -Access to both commands can be limited to a specific Discord permission. This can be configured below, but defaults to "Manage Messages". +Access to both commands can be limited to a specific Discord permission. This can be configured below, but defaults to " +Manage Messages". # Configuration @@ -45,13 +49,18 @@ To configure this module, values can be provided within the `extPhishing` builde * **Required:** `appName` - Give your application a name so that Sinking Yachts can identify it for statistics purposes * `detectionAction` (default: Delete) - What to do when a message containing a phishing domain is detected -* `logChannelName` (default: "logs") - The name of the channel to use for logging; the extension will search the channels present on the current server and use the last one with an exactly-matching name -* `notifyUser` (default: True) - Whether to DM the user, letting them know they posted a phishing domain and what action was taken -* `requiredCommandPermission` (default: Manage Server) - The permission a user must have in order to run the bundled message and slash commands +* `logChannelName` (default: "logs") - The name of the channel to use for logging; the extension will search the + channels present on the current server and use the last one with an exactly-matching name +* `notifyUser` (default: True) - Whether to DM the user, letting them know they posted a phishing domain and what action + was taken +* `requiredCommandPermission` (default: Manage Server) - The permission a user must have in order to run the bundled + message and slash commands * `updateDelay` (default: 15 minutes) - How often to check for new phishing domains, five minutes at minimum -* `urlRegex` (default: [The Perfect URL Regex](https://urlregex.com/)) - A regular expression used to extract domains from messages, with **exactly one capturing group containing the entire domain** (and optionally the URL path) +* `urlRegex` (default: [The Perfect URL Regex](https://urlregex.com/)) - A regular expression used to extract domains + from messages, with **exactly one capturing group containing the entire domain** (and optionally the URL path) Additionally, the following configuration functions are available: -* `check` - Used to define checks that must pass for event handlers to be run, and thus for messages to be checked (you could use this to exempt your staff, for example) +* `check` - Used to define checks that must pass for event handlers to be run, and thus for messages to be checked (you + could use this to exempt your staff, for example) * `regex` - A convenience function for registering a `String` as URL regex, with the case-insensitive flag diff --git a/extra-modules/extra-phishing/build.gradle.kts b/extra-modules/extra-phishing/build.gradle.kts index 4cb466b418..510fd41548 100644 --- a/extra-modules/extra-phishing/build.gradle.kts +++ b/extra-modules/extra-phishing/build.gradle.kts @@ -1,34 +1,34 @@ plugins { - `kordex-module` - `published-module` - `disable-explicit-api-mode` + `kordex-module` + `published-module` + `disable-explicit-api-mode` - kotlin("plugin.serialization") + kotlin("plugin.serialization") } metadata { - name = "KordEx Extra: Phishing" - description = "KordEx extra module that provides anti-phishing functionality for bots" + name = "KordEx Extra: Phishing" + description = "KordEx extra module that provides anti-phishing functionality for bots" } repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } } dependencies { - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - implementation(libs.jsoup) + implementation(libs.jsoup) - implementation(libs.bundles.logging) - implementation(libs.kotlin.stdlib) - implementation(libs.ktor.logging) + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) + implementation(libs.ktor.logging) - implementation(project(":kord-extensions")) + implementation(project(":kord-extensions")) } group = "com.kotlindiscord.kord.extensions" diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DetectionAction.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DetectionAction.kt index 36dd3f70b4..b1c96420cf 100644 --- a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DetectionAction.kt +++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DetectionAction.kt @@ -14,15 +14,15 @@ package com.kotlindiscord.kord.extensions.modules.extra.phishing * @property message Message to return to the user. */ sealed class DetectionAction(val message: String) { - /** Ban 'em and delete the message. **/ - object Ban : DetectionAction("you have been banned from the server") + /** Ban 'em and delete the message. **/ + object Ban : DetectionAction("you have been banned from the server") - /** Delete the message. **/ - object Delete : DetectionAction("it has been deleted") + /** Delete the message. **/ + object Delete : DetectionAction("it has been deleted") - /** Kick 'em and delete the message. **/ - object Kick : DetectionAction("you have been kicked from the server") + /** Kick 'em and delete the message. **/ + object Kick : DetectionAction("you have been kicked from the server") - /** Don't do anything, just log it in the logs channel. **/ - object LogOnly : DetectionAction("it has been logged for the server staff to review") + /** Don't do anything, just log it in the logs channel. **/ + object LogOnly : DetectionAction("it has been logged for the server staff to review") } diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DomainChange.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DomainChange.kt index f7f1d9703e..19409b264d 100644 --- a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DomainChange.kt +++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DomainChange.kt @@ -17,8 +17,8 @@ import kotlinx.serialization.Serializable */ @Serializable data class DomainChange( - val type: DomainChangeType, - val domains: Set + val type: DomainChangeType, + val domains: Set, ) /** @@ -28,9 +28,9 @@ data class DomainChange( */ @Serializable enum class DomainChangeType(val readableName: String) { - @SerialName("add") - Add("added"), + @SerialName("add") + Add("added"), - @SerialName("delete") - Delete("deleted") + @SerialName("delete") + Delete("deleted") } diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/ExtPhishingBuilder.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/ExtPhishingBuilder.kt index e42dae9ea7..423e7d85ba 100644 --- a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/ExtPhishingBuilder.kt +++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/ExtPhishingBuilder.kt @@ -17,75 +17,75 @@ import kotlin.time.ExperimentalTime /** Builder used to configure the phishing extension. **/ class ExtPhishingBuilder { - /** The name of your application, which allows the Sinking Yachts maintainers to identify what it is. **/ - lateinit var appName: String - - /** Delay between domain update checks, 5 minutes at minimum. **/ - var updateDelay = 15.minutes - - /** - * Regular expression used to extract domains from messages. - * - * If you customize this, it must contain exactly one capturing group, containing the full domain name, and - * optionally the path, if this is an actual URL. You can mark a group as non-capturing by prefixing it with - * `?:`. - * - * The provided regex comes from https://urlregex.com/ - but you can provide a different regex if you need - * detection to be more sensitive than just clickable links. - */ - var urlRegex = "(?:https?|ftp|file|discord)://([-a-zA-Z\\d+&@#/%?=~_|!:,.;]*[-a-zA-Z\\d+&@#/%=~_|])" - .toRegex(RegexOption.IGNORE_CASE) - - /** @suppress List of checks to apply to event handlers. **/ - val checks: MutableList> = mutableListOf() - - /** - * If you want to require a permission for the phishing check commands, supply it here. Alternatively, supply - * `null` and everyone will be given access to them. - */ - var requiredCommandPermission: Permission? = Permission.ManageMessages - - /** - * What to do when a message creation/edit contains a phishing domain. - * - * @see DetectionAction - */ - var detectionAction: DetectionAction = DetectionAction.Delete - - /** Whether to DM users when their messages contain phishing domains, with the action taken. **/ - var notifyUser = true - - /** - * The name of the logs channel to use for detection messages, if not "logs". - * - * The extension will try to find the last channel in the channel list with a name exactly matching the - * given name here, "logs" by default. - */ - var logChannelName = "logs" - - /** Register a check that must pass in order for an event handler to run, and for messages to be processed. **/ - fun check(check: CheckWithCache) { - checks.add(check) - } - - /** Register checks that must pass in order for an event handler to run, and for messages to be processed. **/ - fun check(vararg checkList: CheckWithCache) { - checks.addAll(checkList) - } - - /** Convenience function for supplying a case-insensitive [urlRegex]. **/ - fun regex(pattern: String) { - urlRegex = pattern.toRegex(RegexOption.IGNORE_CASE) - } - - /** @suppress **/ - fun validate() { - if (!this::appName.isInitialized) { - error("Application name must be provided") - } - - if (updateDelay < 5.minutes) { - error("The update delay must be at least five minutes - don't spam the API!") - } - } + /** The name of your application, which allows the Sinking Yachts maintainers to identify what it is. **/ + lateinit var appName: String + + /** Delay between domain update checks, 5 minutes at minimum. **/ + var updateDelay = 15.minutes + + /** + * Regular expression used to extract domains from messages. + * + * If you customize this, it must contain exactly one capturing group, containing the full domain name, and + * optionally the path, if this is an actual URL. You can mark a group as non-capturing by prefixing it with + * `?:`. + * + * The provided regex comes from https://urlregex.com/ - but you can provide a different regex if you need + * detection to be more sensitive than just clickable links. + */ + var urlRegex = "(?:https?|ftp|file|discord)://([-a-zA-Z\\d+&@#/%?=~_|!:,.;]*[-a-zA-Z\\d+&@#/%=~_|])" + .toRegex(RegexOption.IGNORE_CASE) + + /** @suppress List of checks to apply to event handlers. **/ + val checks: MutableList> = mutableListOf() + + /** + * If you want to require a permission for the phishing check commands, supply it here. Alternatively, supply + * `null` and everyone will be given access to them. + */ + var requiredCommandPermission: Permission? = Permission.ManageMessages + + /** + * What to do when a message creation/edit contains a phishing domain. + * + * @see DetectionAction + */ + var detectionAction: DetectionAction = DetectionAction.Delete + + /** Whether to DM users when their messages contain phishing domains, with the action taken. **/ + var notifyUser = true + + /** + * The name of the logs channel to use for detection messages, if not "logs". + * + * The extension will try to find the last channel in the channel list with a name exactly matching the + * given name here, "logs" by default. + */ + var logChannelName = "logs" + + /** Register a check that must pass in order for an event handler to run, and for messages to be processed. **/ + fun check(check: CheckWithCache) { + checks.add(check) + } + + /** Register checks that must pass in order for an event handler to run, and for messages to be processed. **/ + fun check(vararg checkList: CheckWithCache) { + checks.addAll(checkList) + } + + /** Convenience function for supplying a case-insensitive [urlRegex]. **/ + fun regex(pattern: String) { + urlRegex = pattern.toRegex(RegexOption.IGNORE_CASE) + } + + /** @suppress **/ + fun validate() { + if (!this::appName.isInitialized) { + error("Application name must be provided") + } + + if (updateDelay < 5.minutes) { + error("The update delay must be at least five minutes - don't spam the API!") + } + } } diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/Functions.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/Functions.kt index 0e9d7bdacc..c5f8c18c3a 100644 --- a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/Functions.kt +++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/Functions.kt @@ -12,11 +12,11 @@ import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder * Configure the phishing extension and add it to the bot. */ inline fun ExtensibleBotBuilder.ExtensionsBuilder.extPhishing(builder: ExtPhishingBuilder.() -> Unit) { - val settings = ExtPhishingBuilder() + val settings = ExtPhishingBuilder() - builder(settings) + builder(settings) - settings.validate() + settings.validate() - add { PhishingExtension(settings) } + add { PhishingExtension(settings) } } diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingApi.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingApi.kt index a630a05e95..3a66a320ca 100644 --- a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingApi.kt +++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingApi.kt @@ -21,37 +21,37 @@ internal const val SIZE_PATH = "https://phish.sinking.yachts/v2/dbsize" /** Implementation of the Sinking Yachts phishing domain API. **/ class PhishingApi(internal val appName: String) { - internal val client = HttpClient { - install(ContentNegotiation) { - json() - } + internal val client = HttpClient { + install(ContentNegotiation) { + json() + } - install(WebSockets) + install(WebSockets) - expectSuccess = true - } + expectSuccess = true + } - internal suspend inline fun get(url: String): T = client.get(url) { - header("X-Identity", "$appName (via Kord Extensions)") - }.body() + internal suspend inline fun get(url: String): T = client.get(url) { + header("X-Identity", "$appName (via Kord Extensions)") + }.body() - /** Get all known phishing domains from the API. **/ - suspend fun getAllDomains(): Set = - get(ALL_PATH) + /** Get all known phishing domains from the API. **/ + suspend fun getAllDomains(): Set = + get(ALL_PATH) - /** Query the API directly to check a specific domain. **/ - suspend fun checkDomain(domain: String): Boolean = - get(CHECK_PATH.replace("%", domain)) + /** Query the API directly to check a specific domain. **/ + suspend fun checkDomain(domain: String): Boolean = + get(CHECK_PATH.replace("%", domain)) - /** Get all new phishing domains added in the previous [seconds] seconds. **/ - suspend fun getRecentDomains(seconds: Long): List = - get(RECENT_PATH.replace("%", seconds.toString())) + /** Get all new phishing domains added in the previous [seconds] seconds. **/ + suspend fun getRecentDomains(seconds: Long): List = + get(RECENT_PATH.replace("%", seconds.toString())) - /** Get the total number of phishing domains that the API knows about. **/ - suspend fun getTotalDomains(): Long = - get(SIZE_PATH) + /** Get the total number of phishing domains that the API knows about. **/ + suspend fun getTotalDomains(): Long = + get(SIZE_PATH) - /** Connect to the websocket and register a callback to receive changes. Returns a lifecycle wrapper. **/ - fun websocket(callback: suspend (DomainChange) -> Unit) = - PhishingWebsocketWrapper(appName, callback) + /** Connect to the websocket and register a callback to receive changes. Returns a lifecycle wrapper. **/ + fun websocket(callback: suspend (DomainChange) -> Unit) = + PhishingWebsocketWrapper(appName, callback) } diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingExtension.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingExtension.kt index 641672d2bc..4bc821a9d1 100644 --- a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingExtension.kt +++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingExtension.kt @@ -44,365 +44,365 @@ const val MAX_REDIRECTS = 5 /** Phishing extension, responsible for checking for phishing domains in messages. **/ class PhishingExtension(private val settings: ExtPhishingBuilder) : Extension() { - override val name = "phishing" - - private val api = PhishingApi(settings.appName) - private val domainCache: MutableSet = mutableSetOf() - private val logger = KotlinLogging.logger { } - - private var websocket: PhishingWebsocketWrapper = api.websocket(::handleChange) - - private val httpClient = HttpClient { - followRedirects = false - expectSuccess = true - } - - override suspend fun setup() { - websocket.stop() - websocket.start() - - domainCache.addAll(api.getAllDomains()) - - event { - check { isNotBot() } - check { anyGuild() } - - check { - settings.checks.forEach { - if (passed) it() - } - } - - action { - handleMessage(event.message.asMessageOrNull()) - } - } - - event { - check { isNotBot() } - check { anyGuild() } - - check { - settings.checks.forEach { - if (passed) it() - } - } - - action { - handleMessage(event.message.asMessageOrNull()) - } - } - - ephemeralMessageCommand { - name = "Phishing Check" - - if (this@PhishingExtension.settings.requiredCommandPermission != null) { - check { hasPermission(this@PhishingExtension.settings.requiredCommandPermission!!) } - } - - action { - for (message in targetMessages) { - val matches = parseDomains(message.content) - - respond { - content = if (matches.isNotEmpty()) { - "⚠️ [Message ${message.id.value}](${message.getJumpUrl()}) " + - "**contains ${matches.size} phishing link/s**." - } else { - "✅ [Message ${message.id.value}](${message.getJumpUrl()}) " + - "**does not contain any phishing links**." - } - } - } - } - } - - ephemeralSlashCommand(::DomainArgs) { - name = "phishing-check" - description = "Check whether a given domain is a known phishing domain." - - if (this@PhishingExtension.settings.requiredCommandPermission != null) { - check { hasPermission(this@PhishingExtension.settings.requiredCommandPermission!!) } - } - - action { - respond { - content = if (domainCache.contains(arguments.domain.lowercase())) { - "⚠️ `${arguments.domain}` is a known phishing domain." - } else { - "✅ `${arguments.domain}` is not a known phishing domain." - } - } - } - } - } - - internal suspend fun handleMessage(message: Message?) { - if (message == null) { - return - } - - val matches = parseDomains(message.content) - - if (matches.isNotEmpty()) { - logger.debug { "Found a message with ${matches.size} phishing domains." } - - if (settings.notifyUser) { - message.kord.launch { - message.author!!.dm { - content = "We've detected that the following message contains a phishing domain. For this " + - "reason, **${settings.detectionAction.message}**." - - embed { - title = "Phishing domain detected" - description = message.content - color = DISCORD_RED - - field { - inline = true - - name = "Channel" - value = message.channel.mention - } - - field { - inline = true - - name = "Message ID" - value = "`${message.id.value}`" - } - - field { - inline = true - - name = "Server" - value = message.getGuildOrNull()?.name ?: "Unable to get guild" - } - } - } - } - } - - when (settings.detectionAction) { - DetectionAction.Ban -> { - message.getAuthorAsMemberOrNull()!!.ban { - reason = "Message contained a phishing domain" - } - - message.delete("Message contained a phishing domain") - } - - DetectionAction.Delete -> message.delete("Message contained a phishing domain") - - DetectionAction.Kick -> { - message.getAuthorAsMemberOrNull()!!.kick("Message contained a phishing domain") - message.delete("Message contained a phishing domain") - } - - DetectionAction.LogOnly -> { - // Do nothing, we always log - } - } - - logDeletion(message, matches) - } - } - - internal suspend fun logDeletion(message: Message, matches: Set) { - val guild = message.getGuild() - - val channel = message - .getGuild() - .channels - .filter { it.name == settings.logChannelName } - .lastOrNull() - ?.asChannelOrNull() as? GuildMessageChannel - - if (channel == null) { - logger.warn { - "Unable to find a channel named ${settings.logChannelName} on ${guild.name} (${guild.id.value})" - } - - return - } - - val matchList = "# Phishing Domain Matches\n\n" + - "**Total:** ${matches.size}\n\n" + - matches.joinToString("\n") { "* `$it`" } - - channel.createMessage { - addFile( - "matches.md", - ChannelProvider { matchList.byteInputStream().toByteReadChannel() } - ) - - embed { - title = "Phishing domain detected" - description = message.content - color = DISCORD_RED - - field { - inline = true - - name = "Author" - value = "${message.author!!.mention} (" + - "`${message.author!!.tag}` / " + - "`${message.author!!.id.value}`" + - ")" - } - - field { - inline = true - - name = "Channel" - value = "${message.channel.mention} (`${message.channelId.value}`)" - } - - field { - inline = true - - name = "Message" - value = "[`${message.id.value}`](${message.getJumpUrl()})" - } - - field { - inline = true - - name = "Total Matches" - value = matches.size.toString() - } - } - } - } - - internal suspend fun parseDomains(content: String): MutableSet { - val domains: MutableSet = mutableSetOf() - - for (match in settings.urlRegex.findAll(content)) { - val found = match.groups[1]?.value?.trim('/') ?: continue - - var domain = found - - if ("/" in domain) { - domain = domain - .split("/", limit = 2) - .firstOrNull() - ?.lowercase() ?: continue - } - - domain = domain.filter { it.isLetterOrDigit() || it in "-+&@#%?=~_|!:,.;" } - - if (domain in domainCache) { - domains.add(domain) - } else { - val result = followRedirects(match.value) - ?.split("://") - ?.lastOrNull() - ?.split("/") - ?.first() - ?.lowercase() - - if (result in domainCache && result != null) { - domains.add(result) - } - } - } - - logger.debug { "Found ${domains.size} domains: ${domains.joinToString()}" } - - return domains - } - - @Suppress("MagicNumber", "TooGenericExceptionCaught") // HTTP status codes - internal suspend fun followRedirects(url: String, count: Int = 0): String? { - if (count >= MAX_REDIRECTS) { - logger.warn { "Maximum redirect count reached for URL: $url" } - - return url - } - - val response: HttpResponse = try { - httpClient.get(url) - } catch (e: RedirectResponseException) { - e.response - } catch (e: ClientRequestException) { - val status = e.response.status - - if (status.value !in 200 until 499) { - logger.warn { "$url -> $status" } - } - - return url - } catch (e: Exception) { - logger.warn(e) { url } - - return url - } - - if (response.headers.contains("Location")) { - val newUrl = response.headers["Location"]!! - - if (url.trim('/') == newUrl.trim('/')) { - return null // Results in the same URL - } - - return followRedirects(newUrl, count + 1) - } else { - val soup = try { - Jsoup.connect(url).get() - } catch (e: Exception) { - logger.debug(e) { e.message } - - return url - } - - val element = soup.head() - .getElementsByAttributeValue("http-equiv", "refresh") - .first() - - if (element != null) { - val content = element.attributes().get("content") - - val newUrl = content - .split(";") - .firstOrNull { it.startsWith("URL=", true) } - ?.split("=", limit = 2) - ?.lastOrNull() - - if (newUrl != null) { - if (url.trim('/') == newUrl.trim('/')) { - return null // Results in the same URL - } - - return followRedirects(newUrl, count + 1) - } - } - } - - return url - } - - override suspend fun unload() { - websocket.stop() - } - - internal fun handleChange(change: DomainChange) { - when (change.type) { - DomainChangeType.Add -> domainCache.addAll(change.domains) - DomainChangeType.Delete -> domainCache.removeAll(change.domains) - } - } - - /** Arguments class for domain-relevant commands. **/ - inner class DomainArgs : Arguments() { - /** Targeted domain string. **/ - val domain by string { - name = "domain" - description = "Domain to check" - - validate { - failIf("Please provide the domain name only, without the protocol or a path.") { "/" in value } - } - } - } + override val name = "phishing" + + private val api = PhishingApi(settings.appName) + private val domainCache: MutableSet = mutableSetOf() + private val logger = KotlinLogging.logger { } + + private var websocket: PhishingWebsocketWrapper = api.websocket(::handleChange) + + private val httpClient = HttpClient { + followRedirects = false + expectSuccess = true + } + + override suspend fun setup() { + websocket.stop() + websocket.start() + + domainCache.addAll(api.getAllDomains()) + + event { + check { isNotBot() } + check { anyGuild() } + + check { + settings.checks.forEach { + if (passed) it() + } + } + + action { + handleMessage(event.message.asMessageOrNull()) + } + } + + event { + check { isNotBot() } + check { anyGuild() } + + check { + settings.checks.forEach { + if (passed) it() + } + } + + action { + handleMessage(event.message.asMessageOrNull()) + } + } + + ephemeralMessageCommand { + name = "Phishing Check" + + if (this@PhishingExtension.settings.requiredCommandPermission != null) { + check { hasPermission(this@PhishingExtension.settings.requiredCommandPermission!!) } + } + + action { + for (message in targetMessages) { + val matches = parseDomains(message.content) + + respond { + content = if (matches.isNotEmpty()) { + "⚠️ [Message ${message.id.value}](${message.getJumpUrl()}) " + + "**contains ${matches.size} phishing link/s**." + } else { + "✅ [Message ${message.id.value}](${message.getJumpUrl()}) " + + "**does not contain any phishing links**." + } + } + } + } + } + + ephemeralSlashCommand(::DomainArgs) { + name = "phishing-check" + description = "Check whether a given domain is a known phishing domain." + + if (this@PhishingExtension.settings.requiredCommandPermission != null) { + check { hasPermission(this@PhishingExtension.settings.requiredCommandPermission!!) } + } + + action { + respond { + content = if (domainCache.contains(arguments.domain.lowercase())) { + "⚠️ `${arguments.domain}` is a known phishing domain." + } else { + "✅ `${arguments.domain}` is not a known phishing domain." + } + } + } + } + } + + internal suspend fun handleMessage(message: Message?) { + if (message == null) { + return + } + + val matches = parseDomains(message.content) + + if (matches.isNotEmpty()) { + logger.debug { "Found a message with ${matches.size} phishing domains." } + + if (settings.notifyUser) { + message.kord.launch { + message.author!!.dm { + content = "We've detected that the following message contains a phishing domain. For this " + + "reason, **${settings.detectionAction.message}**." + + embed { + title = "Phishing domain detected" + description = message.content + color = DISCORD_RED + + field { + inline = true + + name = "Channel" + value = message.channel.mention + } + + field { + inline = true + + name = "Message ID" + value = "`${message.id.value}`" + } + + field { + inline = true + + name = "Server" + value = message.getGuildOrNull()?.name ?: "Unable to get guild" + } + } + } + } + } + + when (settings.detectionAction) { + DetectionAction.Ban -> { + message.getAuthorAsMemberOrNull()!!.ban { + reason = "Message contained a phishing domain" + } + + message.delete("Message contained a phishing domain") + } + + DetectionAction.Delete -> message.delete("Message contained a phishing domain") + + DetectionAction.Kick -> { + message.getAuthorAsMemberOrNull()!!.kick("Message contained a phishing domain") + message.delete("Message contained a phishing domain") + } + + DetectionAction.LogOnly -> { + // Do nothing, we always log + } + } + + logDeletion(message, matches) + } + } + + internal suspend fun logDeletion(message: Message, matches: Set) { + val guild = message.getGuild() + + val channel = message + .getGuild() + .channels + .filter { it.name == settings.logChannelName } + .lastOrNull() + ?.asChannelOrNull() as? GuildMessageChannel + + if (channel == null) { + logger.warn { + "Unable to find a channel named ${settings.logChannelName} on ${guild.name} (${guild.id.value})" + } + + return + } + + val matchList = "# Phishing Domain Matches\n\n" + + "**Total:** ${matches.size}\n\n" + + matches.joinToString("\n") { "* `$it`" } + + channel.createMessage { + addFile( + "matches.md", + ChannelProvider { matchList.byteInputStream().toByteReadChannel() } + ) + + embed { + title = "Phishing domain detected" + description = message.content + color = DISCORD_RED + + field { + inline = true + + name = "Author" + value = "${message.author!!.mention} (" + + "`${message.author!!.tag}` / " + + "`${message.author!!.id.value}`" + + ")" + } + + field { + inline = true + + name = "Channel" + value = "${message.channel.mention} (`${message.channelId.value}`)" + } + + field { + inline = true + + name = "Message" + value = "[`${message.id.value}`](${message.getJumpUrl()})" + } + + field { + inline = true + + name = "Total Matches" + value = matches.size.toString() + } + } + } + } + + internal suspend fun parseDomains(content: String): MutableSet { + val domains: MutableSet = mutableSetOf() + + for (match in settings.urlRegex.findAll(content)) { + val found = match.groups[1]?.value?.trim('/') ?: continue + + var domain = found + + if ("/" in domain) { + domain = domain + .split("/", limit = 2) + .firstOrNull() + ?.lowercase() ?: continue + } + + domain = domain.filter { it.isLetterOrDigit() || it in "-+&@#%?=~_|!:,.;" } + + if (domain in domainCache) { + domains.add(domain) + } else { + val result = followRedirects(match.value) + ?.split("://") + ?.lastOrNull() + ?.split("/") + ?.first() + ?.lowercase() + + if (result in domainCache && result != null) { + domains.add(result) + } + } + } + + logger.debug { "Found ${domains.size} domains: ${domains.joinToString()}" } + + return domains + } + + @Suppress("MagicNumber", "TooGenericExceptionCaught") // HTTP status codes + internal suspend fun followRedirects(url: String, count: Int = 0): String? { + if (count >= MAX_REDIRECTS) { + logger.warn { "Maximum redirect count reached for URL: $url" } + + return url + } + + val response: HttpResponse = try { + httpClient.get(url) + } catch (e: RedirectResponseException) { + e.response + } catch (e: ClientRequestException) { + val status = e.response.status + + if (status.value !in 200 until 499) { + logger.warn { "$url -> $status" } + } + + return url + } catch (e: Exception) { + logger.warn(e) { url } + + return url + } + + if (response.headers.contains("Location")) { + val newUrl = response.headers["Location"]!! + + if (url.trim('/') == newUrl.trim('/')) { + return null // Results in the same URL + } + + return followRedirects(newUrl, count + 1) + } else { + val soup = try { + Jsoup.connect(url).get() + } catch (e: Exception) { + logger.debug(e) { e.message } + + return url + } + + val element = soup.head() + .getElementsByAttributeValue("http-equiv", "refresh") + .first() + + if (element != null) { + val content = element.attributes().get("content") + + val newUrl = content + .split(";") + .firstOrNull { it.startsWith("URL=", true) } + ?.split("=", limit = 2) + ?.lastOrNull() + + if (newUrl != null) { + if (url.trim('/') == newUrl.trim('/')) { + return null // Results in the same URL + } + + return followRedirects(newUrl, count + 1) + } + } + } + + return url + } + + override suspend fun unload() { + websocket.stop() + } + + internal fun handleChange(change: DomainChange) { + when (change.type) { + DomainChangeType.Add -> domainCache.addAll(change.domains) + DomainChangeType.Delete -> domainCache.removeAll(change.domains) + } + } + + /** Arguments class for domain-relevant commands. **/ + inner class DomainArgs : Arguments() { + /** Targeted domain string. **/ + val domain by string { + name = "domain" + description = "Domain to check" + + validate { + failIf("Please provide the domain name only, without the protocol or a path.") { "/" in value } + } + } + } } diff --git a/extra-modules/extra-pluralkit/README.md b/extra-modules/extra-pluralkit/README.md index bcce7b726e..c6f65f1fee 100644 --- a/extra-modules/extra-pluralkit/README.md +++ b/extra-modules/extra-pluralkit/README.md @@ -3,7 +3,7 @@ [![Discord: Click here](https://img.shields.io/static/v1?label=Discord&message=Click%20here&color=7289DA&style=for-the-badge&logo=discord)](https://discord.gg/ZKRetPNtvY)
![Latest](https://img.shields.io/maven-metadata/v?label=Latest&metadataUrl=https%3A%2F%2Fs01.oss.sonatype.org%2Fservice%2Flocal%2Frepositories%2Fsnapshots%2Fcontent%2Fcom%2Fkotlindiscord%2Fkord%2Fextensions%2Fkord-extensions%2Fmaven-metadata.xml&style=for-the-badge) -This module contains an extension intended to ease the development of bots that wish to support +This module contains an extension intended to ease the development of bots that wish to support [PluralKit](https://pluralkit.me/) users. PluralKit is a Discord bot that attempts to make Discord more comfortable to use for [plural systems](https://morethanone.info), and other people that may benefit from using it as a mental health aid. @@ -16,7 +16,8 @@ handling for chat commands. # Getting Started -* **Maven repo:** Maven Central for releases, `https://s01.oss.sonatype.org/content/repositories/snapshots` for snapshots +* **Maven repo:** Maven Central for releases, `https://s01.oss.sonatype.org/content/repositories/snapshots` for + snapshots * **Maven coordinates:** `com.kotlindiscord.kord.extensions:extra-pluralkit:VERSION` At its simplest, you can add this extension directly to your bot with a minimum configuration. For example: @@ -34,7 +35,7 @@ suspend fun main() { } ``` -This will install the extension using its default configuration. However, the extension may be configured in several +This will install the extension using its default configuration. However, the extension may be configured in several ways - as is detailed below. Your bot will now require the **Manage Webhooks** permission to use the extension. @@ -53,7 +54,7 @@ changed or retrieved if you have the **Manage Server** permission on the current * `api-url`: If you're using a fork of PluralKit or running your own instance, you can provide its API base URL here. This should not include the version in the URL - `https://api.pluralkit.me` is the default base URL, and does not include the `/v2` at the end. -* `bot`: If you're using a fork of PluralKit or running your own instance, you can provide the bot's account here. The +* `bot`: If you're using a fork of PluralKit or running your own instance, you can provide the bot's account here. The extension will not handle proxying for messages proxied by other bots. * `toggle-support`: You can use this option to forcibly disable PK support on the current server, if needed. diff --git a/extra-modules/extra-pluralkit/build.gradle.kts b/extra-modules/extra-pluralkit/build.gradle.kts index e844ea61bf..a5064c7666 100644 --- a/extra-modules/extra-pluralkit/build.gradle.kts +++ b/extra-modules/extra-pluralkit/build.gradle.kts @@ -1,32 +1,32 @@ plugins { - `kordex-module` - `published-module` - `disable-explicit-api-mode` + `kordex-module` + `published-module` + `disable-explicit-api-mode` - kotlin("plugin.serialization") + kotlin("plugin.serialization") } metadata { - name = "KordEx Extra: PluralKit" - description = "KordEx extra module that provides PluralKit event functionality for bots" + name = "KordEx Extra: PluralKit" + description = "KordEx extra module that provides PluralKit event functionality for bots" } repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } } dependencies { - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - implementation(libs.bundles.logging) - implementation(libs.kotlin.stdlib) - implementation(libs.ktor.logging) + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) + implementation(libs.ktor.logging) - implementation(project(":kord-extensions")) + implementation(project(":kord-extensions")) } group = "com.kotlindiscord.kord.extensions" diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/PKExtension.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/PKExtension.kt index 028d3d698b..b610072649 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/PKExtension.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/PKExtension.kt @@ -5,10 +5,10 @@ */ @file:Suppress( - "MagicNumber", - "UndocumentedPublicClass", - "UndocumentedPublicFunction", - "UndocumentedPublicProperty", + "MagicNumber", + "UndocumentedPublicClass", + "UndocumentedPublicFunction", + "UndocumentedPublicProperty", ) package com.kotlindiscord.kord.extensions.modules.extra.pluralkit @@ -60,508 +60,508 @@ const val POSITIVE_EMOTE = "✅" @Suppress("StringLiteralDuplication") class PKExtension : Extension() { - override val name: String = "ext-pluralkit" - override val bundle: String = "kordex.pluralkit" - - private val logger = KotlinLogging.logger( - "com.kotlindiscord.kord.extensions.modules.extra.pluralkit.PKExtension" - ) - - private val guildConfig = StorageUnit( - StorageType.Config, - name, - "guild-config", - PKGuildConfig::class - ) - - private val eventLock = Mutex() - - private val awaitingEvents: MutableMap = mutableMapOf() - private val replyCache: LRUHashMap = LRUHashMap(1000) - - private val scheduler: Scheduler = Scheduler() - private var checkTask: Task? = null - - private val apiMap: MutableStringKeyedMap = mutableMapOf() - - override suspend fun setup() { - checkTask = scheduler.schedule( - 1.seconds, - true, - "pk-check-task", - 1, - repeat = true - ) { - val now = Clock.System.now() - val target = now - 3.seconds // This delay will need fine-tuning over time - - eventLock.withLock { - awaitingEvents - .toMap() - .filterKeys { it.timestamp < target } - .forEach { (key, value) -> - awaitingEvents.remove(key) - - kord.launch { - bot.send(value.unproxied()) - } - } - } - } - - event { - action { - val guild = event.getGuildOrNull() - - if (guild == null) { - kord.launch { - bot.send(event.unproxied()) - } - - return@action - } - - val config = guild.config() - - if (!config.enabled) { - kord.launch { - bot.send(event.unproxied()) - } - - return@action - } - - val message = event.message - val webhookId = message.data.webhookId.value - - if (webhookId == null) { - eventLock.withLock { - awaitingEvents[message.id] = event - } - - val referencedMessage = message.messageReference?.message?.asMessageOrNull() - - if (referencedMessage != null) { - replyCache[message.id] = referencedMessage - } - - return@action - } - - // This is to work around Kord's lack of support for forum channels. This can go once they're supported. - val channel = try { - topChannelFor(event)?.asChannelOfOrNull() - } catch (e: ClassCastException) { - logger.warn(e) { "Failed to cast channel to TopGuildMessageChannel" } - - null - } - - val webhook = try { - channel - ?.asChannelOfOrNull() - ?.webhooks - ?.firstOrNull { it.id == webhookId } - } catch (e: KtorRequestException) { - logger.warn(e) { "Failed to retrieve webhooks for channel: ${channel?.id}" } - - null - } - - if (webhook == null || webhook.creatorId != config.botId) { - return@action - } - - delay(2.seconds) - - val pkMessage = config.api().getMessageOrNull(message.id) - - if (pkMessage == null) { - return@action - } - - eventLock.withLock { - awaitingEvents.remove(pkMessage.original) - awaitingEvents.remove(message.id) - } - - kord.launch { - bot.send( - event.proxied(pkMessage, replyCache[message.id]) - ) - } - } - } - - event { - action { - val guild = event.getGuildOrNull() - - if (guild == null) { - kord.launch { - bot.send(event.unproxied()) - } + override val name: String = "ext-pluralkit" + override val bundle: String = "kordex.pluralkit" + + private val logger = KotlinLogging.logger( + "com.kotlindiscord.kord.extensions.modules.extra.pluralkit.PKExtension" + ) + + private val guildConfig = StorageUnit( + StorageType.Config, + name, + "guild-config", + PKGuildConfig::class + ) + + private val eventLock = Mutex() + + private val awaitingEvents: MutableMap = mutableMapOf() + private val replyCache: LRUHashMap = LRUHashMap(1000) + + private val scheduler: Scheduler = Scheduler() + private var checkTask: Task? = null + + private val apiMap: MutableStringKeyedMap = mutableMapOf() + + override suspend fun setup() { + checkTask = scheduler.schedule( + 1.seconds, + true, + "pk-check-task", + 1, + repeat = true + ) { + val now = Clock.System.now() + val target = now - 3.seconds // This delay will need fine-tuning over time + + eventLock.withLock { + awaitingEvents + .toMap() + .filterKeys { it.timestamp < target } + .forEach { (key, value) -> + awaitingEvents.remove(key) + + kord.launch { + bot.send(value.unproxied()) + } + } + } + } + + event { + action { + val guild = event.getGuildOrNull() + + if (guild == null) { + kord.launch { + bot.send(event.unproxied()) + } + + return@action + } + + val config = guild.config() + + if (!config.enabled) { + kord.launch { + bot.send(event.unproxied()) + } + + return@action + } + + val message = event.message + val webhookId = message.data.webhookId.value + + if (webhookId == null) { + eventLock.withLock { + awaitingEvents[message.id] = event + } + + val referencedMessage = message.messageReference?.message?.asMessageOrNull() + + if (referencedMessage != null) { + replyCache[message.id] = referencedMessage + } + + return@action + } + + // This is to work around Kord's lack of support for forum channels. This can go once they're supported. + val channel = try { + topChannelFor(event)?.asChannelOfOrNull() + } catch (e: ClassCastException) { + logger.warn(e) { "Failed to cast channel to TopGuildMessageChannel" } + + null + } + + val webhook = try { + channel + ?.asChannelOfOrNull() + ?.webhooks + ?.firstOrNull { it.id == webhookId } + } catch (e: KtorRequestException) { + logger.warn(e) { "Failed to retrieve webhooks for channel: ${channel?.id}" } + + null + } + + if (webhook == null || webhook.creatorId != config.botId) { + return@action + } + + delay(2.seconds) + + val pkMessage = config.api().getMessageOrNull(message.id) + + if (pkMessage == null) { + return@action + } + + eventLock.withLock { + awaitingEvents.remove(pkMessage.original) + awaitingEvents.remove(message.id) + } + + kord.launch { + bot.send( + event.proxied(pkMessage, replyCache[message.id]) + ) + } + } + } + + event { + action { + val guild = event.getGuildOrNull() + + if (guild == null) { + kord.launch { + bot.send(event.unproxied()) + } - return@action - } + return@action + } - val config = guild.config() + val config = guild.config() - if (!config.enabled) { - kord.launch { - bot.send(event.unproxied()) - } + if (!config.enabled) { + kord.launch { + bot.send(event.unproxied()) + } - return@action - } + return@action + } - val message = event.message - val webhookId = message?.data?.webhookId?.value + val message = event.message + val webhookId = message?.data?.webhookId?.value - eventLock.withLock { - awaitingEvents.remove(message?.id) - } + eventLock.withLock { + awaitingEvents.remove(message?.id) + } - if (webhookId == null) { - val pkMessage = guild.config().api().getMessageOrNull(event.messageId) - - if (pkMessage != null) { - return@action - } - - kord.launch { - bot.send(event.unproxied()) - } - - return@action - } - - // This is to work around Kord's lack of support for forum channels. This can go once they're supported. - val channel = try { - topChannelFor(event)?.asChannelOfOrNull() - } catch (e: ClassCastException) { - logger.warn(e) { "Failed to cast channel to TopGuildMessageChannel" } - - null - } + if (webhookId == null) { + val pkMessage = guild.config().api().getMessageOrNull(event.messageId) + + if (pkMessage != null) { + return@action + } + + kord.launch { + bot.send(event.unproxied()) + } + + return@action + } + + // This is to work around Kord's lack of support for forum channels. This can go once they're supported. + val channel = try { + topChannelFor(event)?.asChannelOfOrNull() + } catch (e: ClassCastException) { + logger.warn(e) { "Failed to cast channel to TopGuildMessageChannel" } + + null + } - val webhook = try { - channel - ?.asChannelOfOrNull() - ?.webhooks - ?.firstOrNull { it.id == webhookId } - } catch (e: KtorRequestException) { - logger.warn(e) { "Failed to retrieve webhooks for channel: ${channel?.id}" } - - null - } - - if (webhook == null || webhook.creatorId != config.botId) { - kord.launch { - bot.send(event.unproxied()) - } + val webhook = try { + channel + ?.asChannelOfOrNull() + ?.webhooks + ?.firstOrNull { it.id == webhookId } + } catch (e: KtorRequestException) { + logger.warn(e) { "Failed to retrieve webhooks for channel: ${channel?.id}" } + + null + } + + if (webhook == null || webhook.creatorId != config.botId) { + kord.launch { + bot.send(event.unproxied()) + } - return@action - } + return@action + } - delay(2.seconds) + delay(2.seconds) - val pkMessage = config.api().getMessageOrNull(message.id) - ?: return@action + val pkMessage = config.api().getMessageOrNull(message.id) + ?: return@action - kord.launch { - bot.send( - event.proxied(pkMessage, replyCache[message.id]) - ) - } - } - } - - event { - action { - val guild = event.channel.asChannelOfOrNull()?.getGuildOrNull() - - if (guild == null) { - kord.launch { - bot.send(event.unproxied()) - } - - return@action - } - - val config = guild.config() + kord.launch { + bot.send( + event.proxied(pkMessage, replyCache[message.id]) + ) + } + } + } + + event { + action { + val guild = event.channel.asChannelOfOrNull()?.getGuildOrNull() + + if (guild == null) { + kord.launch { + bot.send(event.unproxied()) + } + + return@action + } + + val config = guild.config() - if (!config.enabled) { - kord.launch { - bot.send(event.unproxied()) - } + if (!config.enabled) { + kord.launch { + bot.send(event.unproxied()) + } - return@action - } - - val message = event.message - val webhookId = message.asMessageOrNull()?.data?.webhookId?.value + return@action + } + + val message = event.message + val webhookId = message.asMessageOrNull()?.data?.webhookId?.value - if (webhookId == null) { - kord.launch { - bot.send(event.unproxied()) - } + if (webhookId == null) { + kord.launch { + bot.send(event.unproxied()) + } - return@action - } - - // This is to work around Kord's lack of support for forum channels. This can go once they're supported. - val channel = try { - topChannelFor(event)?.asChannelOfOrNull() - } catch (e: ClassCastException) { - logger.warn(e) { "Failed to cast channel to TopGuildMessageChannel" } - - null - } - - val webhook = try { - channel - ?.asChannelOfOrNull() - ?.webhooks - ?.firstOrNull { it.id == webhookId } - } catch (e: KtorRequestException) { - logger.warn(e) { "Failed to retrieve webhooks for channel: ${channel?.id}" } - - null - } - - if (webhook == null || webhook.creatorId != config.botId) { - kord.launch { - bot.send(event.unproxied()) - } - - return@action - } - - delay(2.seconds) - - val pkMessage = config.api().getMessageOrNull(message.id) - ?: return@action - - kord.launch { - bot.send( - event.proxied(pkMessage, replyCache[message.id]) - ) - } - } - } - - ephemeralSlashCommand { - name = "command.pluralkit.name" - description = "command.pluralkit.description" - - check { anyGuild() } - - ephemeralSubCommand(::ApiUrlArgs) { - name = "command.pluralkit.api-url.name" - description = "command.pluralkit.api-url.description" - - check { hasPermission(Permission.ManageGuild) } - - action { - val guild = getGuild()!! - val config = guild.config() - val configUnit = guild.configUnit() - - if (arguments.url == null) { - respond { - content = translate( - "command.pluralkit.api-url.response.current", - arrayOf(config.apiUrl) - ) - } - - return@action - } - - val resetWords = arrayOf( - "reset", - - translate("arguments.reset"), - - translationsProvider.translate( - "arguments.reset", - bundleName = this@ephemeralSubCommand.bundle - ) - ) - - if (arguments.url in resetWords) { - config.apiUrl = PKGuildConfig().apiUrl - configUnit.save(config) - - respond { - content = translate( - "command.pluralkit.api-url.response.reset", - arrayOf(config.apiUrl) - ) - } - - return@action - } - - config.apiUrl = arguments.url!! - configUnit.save(config) - - respond { - content = translate( - "command.pluralkit.api-url.response.updated", - arrayOf(config.apiUrl) - ) - } - } - } - - ephemeralSubCommand(::BotArgs) { - name = "command.pluralkit.bot.name" - description = "command.pluralkit.bot.description" - - check { hasPermission(Permission.ManageGuild) } - - action { - val guild = getGuild()!! - val config = guild.config() - val configUnit = guild.configUnit() - - if (arguments.bot == null) { - respond { - content = translate( - "command.pluralkit.bot.response.current", - arrayOf("<@${config.botId}>") - ) - } - - return@action - } - - config.botId = arguments.bot!!.id - configUnit.save(config) - - respond { - content = translate( - "command.pluralkit.bot.response.updated", - arrayOf("<@${config.botId}>") - ) - } - } - } - - ephemeralSubCommand { - name = "command.pluralkit.status.name" - description = "command.pluralkit.status.description" - - action { - val config = guild!!.asGuild().config() - - respond { - embed { - title = translate("command.pluralkit.status.response.title") - - description = translate( - "command.pluralkit.status.response.description", - - arrayOf( - config.apiUrl, - "<@${config.botId}>", - config.enabled.emote(), - ) - ) - } - } - } - } - - ephemeralSubCommand(::ToggleSupportArgs) { - name = "command.pluralkit.toggle-support.name" - description = "command.pluralkit.toggle-support.description" - - check { hasPermission(Permission.ManageGuild) } - - action { - val guild = getGuild()!! - val config = guild.config() - val configUnit = guild.configUnit() - - if (arguments.toggle == null) { - respond { - content = translate( - "command.pluralkit.toggle-support.response.current", - arrayOf(config.enabled.emote()) - ) - } - - return@action - } - - config.enabled = arguments.toggle!! - configUnit.save(config) - - respond { - content = translate( - "command.pluralkit.toggle-support.response.updated", - arrayOf(config.enabled.emote()) - ) - } - } - } - } - } - - override suspend fun unload() { - checkTask?.cancel() - checkTask = null - } - - private fun PKGuildConfig.api(): PluralKit { - var api = apiMap[apiUrl] - - if (api == null) { - api = PluralKit(apiUrl) - apiMap[apiUrl] = api - } - - return api - } - - private fun Boolean.emote() = - if (this) { - POSITIVE_EMOTE - } else { - NEGATIVE_EMOTE - } - - private fun GuildBehavior.configUnit() = - guildConfig.withGuild(id) - - private suspend fun GuildBehavior.config(): PKGuildConfig { - val config = configUnit() - - return config.get() - ?: config.save(PKGuildConfig()) - } - - inner class ApiUrlArgs : Arguments() { - val url by optionalString { - name = "argument.api-url.name" - description = "argument.api-url.description" - } - } - - inner class BotArgs : Arguments() { - val bot by optionalUser { - name = "argument.bot.name" - description = "argument.bot.description" - } - } - - inner class ToggleSupportArgs : Arguments() { - val toggle by optionalBoolean { - name = "argument.toggle.name" - description = "argument.toggle.description" - } - } + return@action + } + + // This is to work around Kord's lack of support for forum channels. This can go once they're supported. + val channel = try { + topChannelFor(event)?.asChannelOfOrNull() + } catch (e: ClassCastException) { + logger.warn(e) { "Failed to cast channel to TopGuildMessageChannel" } + + null + } + + val webhook = try { + channel + ?.asChannelOfOrNull() + ?.webhooks + ?.firstOrNull { it.id == webhookId } + } catch (e: KtorRequestException) { + logger.warn(e) { "Failed to retrieve webhooks for channel: ${channel?.id}" } + + null + } + + if (webhook == null || webhook.creatorId != config.botId) { + kord.launch { + bot.send(event.unproxied()) + } + + return@action + } + + delay(2.seconds) + + val pkMessage = config.api().getMessageOrNull(message.id) + ?: return@action + + kord.launch { + bot.send( + event.proxied(pkMessage, replyCache[message.id]) + ) + } + } + } + + ephemeralSlashCommand { + name = "command.pluralkit.name" + description = "command.pluralkit.description" + + check { anyGuild() } + + ephemeralSubCommand(::ApiUrlArgs) { + name = "command.pluralkit.api-url.name" + description = "command.pluralkit.api-url.description" + + check { hasPermission(Permission.ManageGuild) } + + action { + val guild = getGuild()!! + val config = guild.config() + val configUnit = guild.configUnit() + + if (arguments.url == null) { + respond { + content = translate( + "command.pluralkit.api-url.response.current", + arrayOf(config.apiUrl) + ) + } + + return@action + } + + val resetWords = arrayOf( + "reset", + + translate("arguments.reset"), + + translationsProvider.translate( + "arguments.reset", + bundleName = this@ephemeralSubCommand.bundle + ) + ) + + if (arguments.url in resetWords) { + config.apiUrl = PKGuildConfig().apiUrl + configUnit.save(config) + + respond { + content = translate( + "command.pluralkit.api-url.response.reset", + arrayOf(config.apiUrl) + ) + } + + return@action + } + + config.apiUrl = arguments.url!! + configUnit.save(config) + + respond { + content = translate( + "command.pluralkit.api-url.response.updated", + arrayOf(config.apiUrl) + ) + } + } + } + + ephemeralSubCommand(::BotArgs) { + name = "command.pluralkit.bot.name" + description = "command.pluralkit.bot.description" + + check { hasPermission(Permission.ManageGuild) } + + action { + val guild = getGuild()!! + val config = guild.config() + val configUnit = guild.configUnit() + + if (arguments.bot == null) { + respond { + content = translate( + "command.pluralkit.bot.response.current", + arrayOf("<@${config.botId}>") + ) + } + + return@action + } + + config.botId = arguments.bot!!.id + configUnit.save(config) + + respond { + content = translate( + "command.pluralkit.bot.response.updated", + arrayOf("<@${config.botId}>") + ) + } + } + } + + ephemeralSubCommand { + name = "command.pluralkit.status.name" + description = "command.pluralkit.status.description" + + action { + val config = guild!!.asGuild().config() + + respond { + embed { + title = translate("command.pluralkit.status.response.title") + + description = translate( + "command.pluralkit.status.response.description", + + arrayOf( + config.apiUrl, + "<@${config.botId}>", + config.enabled.emote(), + ) + ) + } + } + } + } + + ephemeralSubCommand(::ToggleSupportArgs) { + name = "command.pluralkit.toggle-support.name" + description = "command.pluralkit.toggle-support.description" + + check { hasPermission(Permission.ManageGuild) } + + action { + val guild = getGuild()!! + val config = guild.config() + val configUnit = guild.configUnit() + + if (arguments.toggle == null) { + respond { + content = translate( + "command.pluralkit.toggle-support.response.current", + arrayOf(config.enabled.emote()) + ) + } + + return@action + } + + config.enabled = arguments.toggle!! + configUnit.save(config) + + respond { + content = translate( + "command.pluralkit.toggle-support.response.updated", + arrayOf(config.enabled.emote()) + ) + } + } + } + } + } + + override suspend fun unload() { + checkTask?.cancel() + checkTask = null + } + + private fun PKGuildConfig.api(): PluralKit { + var api = apiMap[apiUrl] + + if (api == null) { + api = PluralKit(apiUrl) + apiMap[apiUrl] = api + } + + return api + } + + private fun Boolean.emote() = + if (this) { + POSITIVE_EMOTE + } else { + NEGATIVE_EMOTE + } + + private fun GuildBehavior.configUnit() = + guildConfig.withGuild(id) + + private suspend fun GuildBehavior.config(): PKGuildConfig { + val config = configUnit() + + return config.get() + ?: config.save(PKGuildConfig()) + } + + inner class ApiUrlArgs : Arguments() { + val url by optionalString { + name = "argument.api-url.name" + description = "argument.api-url.description" + } + } + + inner class BotArgs : Arguments() { + val bot by optionalUser { + name = "argument.bot.name" + description = "argument.bot.description" + } + } + + inner class ToggleSupportArgs : Arguments() { + val toggle by optionalBoolean { + name = "argument.toggle.name" + description = "argument.toggle.description" + } + } } diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/_Utils.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/_Utils.kt index 536358a91a..38177d89c1 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/_Utils.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/_Utils.kt @@ -10,5 +10,5 @@ import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder /** Set up and add the PluralKit extension to your bot. **/ fun ExtensibleBotBuilder.ExtensionsBuilder.extPluralKit() { - add(::PKExtension) + add(::PKExtension) } diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMember.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMember.kt index 1014d60af4..e00bb65f19 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMember.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMember.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty",) +@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty") package com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api @@ -14,28 +14,28 @@ import kotlinx.serialization.Serializable @Serializable data class PKMember( - val id: String, - val uuid: String, - val name: String, + val id: String, + val uuid: String, + val name: String, - @SerialName("display_name") - val displayName: String?, + @SerialName("display_name") + val displayName: String?, - val color: String?, // PK docs are wrong - val birthday: String?, - val pronouns: String?, + val color: String?, // PK docs are wrong + val birthday: String?, + val pronouns: String?, - @SerialName("avatar_url") - val avatarUrl: String?, + @SerialName("avatar_url") + val avatarUrl: String?, - val banner: String?, - val description: String?, - val created: Instant?, + val banner: String?, + val description: String?, + val created: Instant?, - @SerialName("proxy_tags") - val proxyTags: List, + @SerialName("proxy_tags") + val proxyTags: List, - @SerialName("keep_proxy") - val keepProxy: Boolean, - val privacy: PKMemberPrivacy?, + @SerialName("keep_proxy") + val keepProxy: Boolean, + val privacy: PKMemberPrivacy?, ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMemberPrivacy.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMemberPrivacy.kt index c35b0cd508..38b6bd61aa 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMemberPrivacy.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMemberPrivacy.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty",) +@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty") package com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api @@ -13,23 +13,23 @@ import kotlinx.serialization.Serializable @Serializable data class PKMemberPrivacy( - val visibility: Boolean, + val visibility: Boolean, - @SerialName("name_privacy") - val namePrivacy: Boolean, + @SerialName("name_privacy") + val namePrivacy: Boolean, - @SerialName("description_privacy") - val descriptionPrivacy: Boolean, + @SerialName("description_privacy") + val descriptionPrivacy: Boolean, - @SerialName("birthday_privacy") - val birthdayPrivacy: Boolean, + @SerialName("birthday_privacy") + val birthdayPrivacy: Boolean, - @SerialName("pronoun_privacy") - val pronounPrivacy: Boolean, + @SerialName("pronoun_privacy") + val pronounPrivacy: Boolean, - @SerialName("avatar_privacy") - val avatarPrivacy: Boolean, + @SerialName("avatar_privacy") + val avatarPrivacy: Boolean, - @SerialName("metadata_privacy") - val metadataPrivacy: Boolean, + @SerialName("metadata_privacy") + val metadataPrivacy: Boolean, ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMessage.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMessage.kt index 4bf41b1207..cb1a98870b 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMessage.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKMessage.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty",) +@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty") package com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api @@ -14,12 +14,12 @@ import kotlinx.serialization.Serializable @Serializable data class PKMessage( - val timestamp: Instant, - val id: Snowflake, - val original: Snowflake, - val sender: Snowflake, - val channel: Snowflake, + val timestamp: Instant, + val id: Snowflake, + val original: Snowflake, + val sender: Snowflake, + val channel: Snowflake, - val system: PKSystem? = null, - val member: PKMember? = null, + val system: PKSystem? = null, + val member: PKMember? = null, ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKProxyTag.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKProxyTag.kt index e367e0aa70..a9b6599830 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKProxyTag.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKProxyTag.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty",) +@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty") package com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api @@ -12,6 +12,6 @@ import kotlinx.serialization.Serializable @Serializable data class PKProxyTag( - val prefix: String?, - val suffix: String?, + val prefix: String?, + val suffix: String?, ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKSystem.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKSystem.kt index 36a9a5fbe1..788cf02b18 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKSystem.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKSystem.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty",) +@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty") package com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api @@ -14,18 +14,18 @@ import kotlinx.serialization.Serializable @Serializable data class PKSystem( - val id: String, - val uuid: String, - val name: String?, // PK docs are wrong - val description: String?, - val tag: String?, + val id: String, + val uuid: String, + val name: String?, // PK docs are wrong + val description: String?, + val tag: String?, - @SerialName("avatar_url") - val avatarUrl: String?, + @SerialName("avatar_url") + val avatarUrl: String?, - val banner: String?, - val color: String?, // PK docs are wrong - val created: Instant, - val timezone: String? = null, - val privacy: PKSystemPrivacy?, + val banner: String?, + val color: String?, // PK docs are wrong + val created: Instant, + val timezone: String? = null, + val privacy: PKSystemPrivacy?, ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKSystemPrivacy.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKSystemPrivacy.kt index 621437955b..571dddffdc 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKSystemPrivacy.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PKSystemPrivacy.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty",) +@file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UndocumentedPublicProperty") package com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api @@ -13,23 +13,23 @@ import kotlinx.serialization.Serializable @Serializable data class PKSystemPrivacy( - val visibility: Boolean, + val visibility: Boolean, - @SerialName("name_privacy") - val namePrivacy: Boolean, + @SerialName("name_privacy") + val namePrivacy: Boolean, - @SerialName("description_privacy") - val descriptionPrivacy: Boolean, + @SerialName("description_privacy") + val descriptionPrivacy: Boolean, - @SerialName("birthday_privacy") - val birthdayPrivacy: Boolean, + @SerialName("birthday_privacy") + val birthdayPrivacy: Boolean, - @SerialName("pronoun_privacy") - val pronounPrivacy: Boolean, + @SerialName("pronoun_privacy") + val pronounPrivacy: Boolean, - @SerialName("avatar_privacy") - val avatarPrivacy: Boolean, + @SerialName("avatar_privacy") + val avatarPrivacy: Boolean, - @SerialName("metadata_privacy") - val metadataPrivacy: Boolean, + @SerialName("metadata_privacy") + val metadataPrivacy: Boolean, ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PluralKit.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PluralKit.kt index 66113bace2..d36cf8abb4 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PluralKit.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/api/PluralKit.kt @@ -23,77 +23,77 @@ import kotlinx.serialization.json.Json internal const val PK_API_VERSION = 2 class PluralKit(private val baseUrl: String = "https://api.pluralkit.me", cacheSize: Int = 10_000) { - private val logger = KotlinLogging.logger { } - - private val messageUrl: String = "${this.baseUrl}/v$PK_API_VERSION/messages/{id}" - private val messageCache: LRUHashMap = LRUHashMap(cacheSize) - - private val client = HttpClient { - install(ContentNegotiation) { - json( - Json { ignoreUnknownKeys = true }, - ContentType.Any - ) - } - - expectSuccess = true - } - - suspend fun getMessage(id: Snowflake) = - getMessage(id.toString()) - - @Suppress("MagicNumber") - suspend fun getMessage(id: String): PKMessage { - val cachedMessage = messageCache[id] - - if (cachedMessage != null) { - return cachedMessage - } - - val url = messageUrl.replace("id" to id) - - try { - val result: PKMessage = client.get(url).body() - messageCache[id] = result - - logger.debug { "/messages/$id -> 200" } - - return result - } catch (e: ClientRequestException) { - if (e.response.status.value in 400 until 600) { - if (e.response.status.value == HttpStatusCode.NotFound.value) { - logger.debug { "/messages/$id -> ${e.response.status}" } - } else { - logger.error(e) { "/messages/$id -> ${e.response.status}" } - } - } - - throw e - } - } - - suspend fun getMessageOrNull(id: Snowflake) = - getMessageOrNull(id.toString()) - - suspend fun getMessageOrNull(id: String): PKMessage? { - try { - return getMessage(id) - } catch (e: ClientRequestException) { - if (e.response.status.value != HttpStatusCode.NotFound.value) { - throw e - } - } - - return null - } - - private fun String.replace(vararg pairs: Pair): String { - var result = this - - pairs.forEach { (k, v) -> - result = result.replace("{$k}", v.toString()) - } - - return result - } + private val logger = KotlinLogging.logger { } + + private val messageUrl: String = "${this.baseUrl}/v$PK_API_VERSION/messages/{id}" + private val messageCache: LRUHashMap = LRUHashMap(cacheSize) + + private val client = HttpClient { + install(ContentNegotiation) { + json( + Json { ignoreUnknownKeys = true }, + ContentType.Any + ) + } + + expectSuccess = true + } + + suspend fun getMessage(id: Snowflake) = + getMessage(id.toString()) + + @Suppress("MagicNumber") + suspend fun getMessage(id: String): PKMessage { + val cachedMessage = messageCache[id] + + if (cachedMessage != null) { + return cachedMessage + } + + val url = messageUrl.replace("id" to id) + + try { + val result: PKMessage = client.get(url).body() + messageCache[id] = result + + logger.debug { "/messages/$id -> 200" } + + return result + } catch (e: ClientRequestException) { + if (e.response.status.value in 400 until 600) { + if (e.response.status.value == HttpStatusCode.NotFound.value) { + logger.debug { "/messages/$id -> ${e.response.status}" } + } else { + logger.error(e) { "/messages/$id -> ${e.response.status}" } + } + } + + throw e + } + } + + suspend fun getMessageOrNull(id: Snowflake) = + getMessageOrNull(id.toString()) + + suspend fun getMessageOrNull(id: String): PKMessage? { + try { + return getMessage(id) + } catch (e: ClientRequestException) { + if (e.response.status.value != HttpStatusCode.NotFound.value) { + throw e + } + } + + return null + } + + private fun String.replace(vararg pairs: Pair): String { + var result = this + + pairs.forEach { (k, v) -> + result = result.replace("{$k}", v.toString()) + } + + return result + } } diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageCreateEvent.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageCreateEvent.kt index b24c290b53..7998efe71f 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageCreateEvent.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageCreateEvent.kt @@ -38,36 +38,36 @@ import dev.kord.core.supplier.EntitySupplyStrategy * @property repliedToMessage The original message that was replied to, even if this one was proxied. */ sealed class PKMessageCreateEvent( - open val event: MessageCreateEvent, - override val message: Message, - open val guildId: Snowflake?, - open val author: Member?, - open val repliedToMessage: Message?, - override val shard: Int, - override val supplier: EntitySupplier = event.kord.defaultSupplier, + open val event: MessageCreateEvent, + override val message: Message, + open val guildId: Snowflake?, + open val author: Member?, + open val repliedToMessage: Message?, + override val shard: Int, + override val supplier: EntitySupplier = event.kord.defaultSupplier, ) : KordExEvent, Strategizable, MessageEvent, GuildEvent, MemberEvent, ChannelEvent { - /** @suppress Forwards to [repliedToMessage], **/ - val referencedMessage get() = repliedToMessage + /** @suppress Forwards to [repliedToMessage], **/ + val referencedMessage get() = repliedToMessage - override val guild: GuildBehavior? get() = guildId?.let { kord.unsafe.guild(it) } - override val member: MemberBehavior? get() = author - override val user: UserBehavior? get() = author - override val channel: ChannelBehavior get() = message.channel + override val guild: GuildBehavior? get() = guildId?.let { kord.unsafe.guild(it) } + override val member: MemberBehavior? get() = author + override val user: UserBehavior? get() = author + override val channel: ChannelBehavior get() = message.channel - override suspend fun getGuild(): Guild = getGuildOrNull()!! - override suspend fun getGuildOrNull(): Guild? = guildId?.let { supplier.getGuildOrNull(it) } + override suspend fun getGuild(): Guild = getGuildOrNull()!! + override suspend fun getGuildOrNull(): Guild? = guildId?.let { supplier.getGuildOrNull(it) } - override suspend fun getMember(): Member = author!! - override suspend fun getMemberOrNull(): Member? = author + override suspend fun getMember(): Member = author!! + override suspend fun getMemberOrNull(): Member? = author - override suspend fun getMessage(): Message = message - override suspend fun getMessageOrNull(): Message? = message + override suspend fun getMessage(): Message = message + override suspend fun getMessageOrNull(): Message? = message - override suspend fun getUser(): User = author!! - override suspend fun getUserOrNull(): User? = author + override suspend fun getUser(): User = author!! + override suspend fun getUserOrNull(): User? = author - override suspend fun getChannel(): Channel = channel.asChannel() - override suspend fun getChannelOrNull(): Channel? = getChannel() + override suspend fun getChannel(): Channel = channel.asChannel() + override suspend fun getChannelOrNull(): Channel? = getChannel() } /** @@ -76,100 +76,100 @@ sealed class PKMessageCreateEvent( * @property pkMessage The PluralKit message object with metadata about the proxied message. */ class ProxiedMessageCreateEvent( - event: MessageCreateEvent, - message: Message, - guildId: Snowflake?, - override val author: Member, - repliedToMessage: Message?, - shard: Int, - val pkMessage: PKMessage, - supplier: EntitySupplier = event.kord.defaultSupplier, + event: MessageCreateEvent, + message: Message, + guildId: Snowflake?, + override val author: Member, + repliedToMessage: Message?, + shard: Int, + val pkMessage: PKMessage, + supplier: EntitySupplier = event.kord.defaultSupplier, ) : PKMessageCreateEvent(event, message, guildId, author, repliedToMessage, shard, supplier) { - override fun withStrategy(strategy: EntitySupplyStrategy<*>): PKMessageCreateEvent { - val strategizedEvent = event.withStrategy(strategy) - - return ProxiedMessageCreateEvent( - strategizedEvent, - strategizedEvent.message, - guildId, - author, - repliedToMessage, - shard, - pkMessage, - strategy.supply(kord) - ) - } - - override fun toString(): String = - "ProxiedMessageCreateEvent(" + - "event=$event, " + - "message=$message, " + - "guildId=$guildId, " + - "author=$author, " + - "repliedToMessage=$repliedToMessage, " + - "shard=$shard, " + - "supplier=$supplier" + - ")" + override fun withStrategy(strategy: EntitySupplyStrategy<*>): PKMessageCreateEvent { + val strategizedEvent = event.withStrategy(strategy) + + return ProxiedMessageCreateEvent( + strategizedEvent, + strategizedEvent.message, + guildId, + author, + repliedToMessage, + shard, + pkMessage, + strategy.supply(kord) + ) + } + + override fun toString(): String = + "ProxiedMessageCreateEvent(" + + "event=$event, " + + "message=$message, " + + "guildId=$guildId, " + + "author=$author, " + + "repliedToMessage=$repliedToMessage, " + + "shard=$shard, " + + "supplier=$supplier" + + ")" } /** * A [MessageCreateEvent] wrapper that represents a message that was **not** proxied by PluralKit. */ class UnProxiedMessageCreateEvent( - event: MessageCreateEvent, - message: Message, - guildId: Snowflake?, - author: Member?, - repliedToMessage: Message?, - shard: Int, - supplier: EntitySupplier = event.kord.defaultSupplier, + event: MessageCreateEvent, + message: Message, + guildId: Snowflake?, + author: Member?, + repliedToMessage: Message?, + shard: Int, + supplier: EntitySupplier = event.kord.defaultSupplier, ) : PKMessageCreateEvent(event, message, guildId, author, repliedToMessage, shard, supplier) { - override fun withStrategy(strategy: EntitySupplyStrategy<*>): PKMessageCreateEvent { - val strategizedEvent = event.withStrategy(strategy) - - return UnProxiedMessageCreateEvent( - strategizedEvent, - strategizedEvent.message, - guildId, - author, - repliedToMessage, - shard, - strategy.supply(kord), - ) - } - - override fun toString(): String = - "UnProxiedMessageCreateEvent(" + - "event=$event, " + - "message=$message, " + - "guildId=$guildId, " + - "author=$author, " + - "repliedToMessage=$repliedToMessage, " + - "shard=$shard, " + - "supplier=$supplier" + - ")" + override fun withStrategy(strategy: EntitySupplyStrategy<*>): PKMessageCreateEvent { + val strategizedEvent = event.withStrategy(strategy) + + return UnProxiedMessageCreateEvent( + strategizedEvent, + strategizedEvent.message, + guildId, + author, + repliedToMessage, + shard, + strategy.supply(kord), + ) + } + + override fun toString(): String = + "UnProxiedMessageCreateEvent(" + + "event=$event, " + + "message=$message, " + + "guildId=$guildId, " + + "author=$author, " + + "repliedToMessage=$repliedToMessage, " + + "shard=$shard, " + + "supplier=$supplier" + + ")" } internal suspend fun MessageCreateEvent.proxied(p: PKMessage, referencedMessage: Message?): ProxiedMessageCreateEvent { - val member = getGuildOrNull()!!.getMemberOrNull(p.sender)!! - - return ProxiedMessageCreateEvent( - this, - message, - guildId, - member, - referencedMessage, - shard, - p, - ) + val member = getGuildOrNull()!!.getMemberOrNull(p.sender)!! + + return ProxiedMessageCreateEvent( + this, + message, + guildId, + member, + referencedMessage, + shard, + p, + ) } -internal suspend fun MessageCreateEvent.unproxied(): UnProxiedMessageCreateEvent = - UnProxiedMessageCreateEvent( - this, - message, - guildId, - member, - message.referencedMessage, - shard - ) +internal fun MessageCreateEvent.unproxied(): UnProxiedMessageCreateEvent = + UnProxiedMessageCreateEvent( + this, + message, + guildId, + member, + message.referencedMessage, + shard + ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageDeleteEvent.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageDeleteEvent.kt index 10b9b06c00..1d6e1105f0 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageDeleteEvent.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageDeleteEvent.kt @@ -39,40 +39,40 @@ import dev.kord.core.supplier.EntitySupplyStrategy * @property repliedToMessage The original message that was replied to, even if this one was proxied. */ abstract class PKMessageDeleteEvent( - open val event: MessageDeleteEvent, - open val channelId: Snowflake, - override val message: Message?, - open val guildId: Snowflake?, - open val author: Member?, - open val repliedToMessage: Message?, - override val shard: Int, - override val supplier: EntitySupplier = event.kord.defaultSupplier, + open val event: MessageDeleteEvent, + open val channelId: Snowflake, + override val message: Message?, + open val guildId: Snowflake?, + open val author: Member?, + open val repliedToMessage: Message?, + override val shard: Int, + override val supplier: EntitySupplier = event.kord.defaultSupplier, ) : KordExEvent, Strategizable, ChannelEvent, MessageEvent, GuildEvent, MemberEvent { - /** @suppress Forwards to [repliedToMessage], **/ - val referencedMessage get() = repliedToMessage + /** @suppress Forwards to [repliedToMessage], **/ + val referencedMessage get() = repliedToMessage - /** Channel behaviour representing the channel the message was sent to. **/ - override val channel: MessageChannelBehavior - get() = MessageChannelBehavior(channelId, kord) + /** Channel behaviour representing the channel the message was sent to. **/ + override val channel: MessageChannelBehavior + get() = MessageChannelBehavior(channelId, kord) - override val guild: GuildBehavior? get() = guildId?.let { kord.unsafe.guild(it) } - override val member: MemberBehavior? get() = author - override val user: UserBehavior? get() = author + override val guild: GuildBehavior? get() = guildId?.let { kord.unsafe.guild(it) } + override val member: MemberBehavior? get() = author + override val user: UserBehavior? get() = author - override suspend fun getChannel(): Channel = channel.asChannel() - override suspend fun getChannelOrNull(): Channel = channel.asChannel() + override suspend fun getChannel(): Channel = channel.asChannel() + override suspend fun getChannelOrNull(): Channel = channel.asChannel() - override suspend fun getGuild(): Guild = getGuildOrNull()!! - override suspend fun getGuildOrNull(): Guild? = guildId?.let { supplier.getGuildOrNull(it) } + override suspend fun getGuild(): Guild = getGuildOrNull()!! + override suspend fun getGuildOrNull(): Guild? = guildId?.let { supplier.getGuildOrNull(it) } - override suspend fun getMember(): Member = author!! - override suspend fun getMemberOrNull(): Member? = author + override suspend fun getMember(): Member = author!! + override suspend fun getMemberOrNull(): Member? = author - override suspend fun getMessage(): Message = message!! - override suspend fun getMessageOrNull(): Message? = message + override suspend fun getMessage(): Message = message!! + override suspend fun getMessageOrNull(): Message? = message - override suspend fun getUser(): User = author!! - override suspend fun getUserOrNull(): User? = author + override suspend fun getUser(): User = author!! + override suspend fun getUserOrNull(): User? = author } /** @@ -81,43 +81,43 @@ abstract class PKMessageDeleteEvent( * @property pkMessage The PluralKit message object with metadata about the proxied message. */ class ProxiedMessageDeleteEvent( - event: MessageDeleteEvent, - channelId: Snowflake, - message: Message?, - guildId: Snowflake?, - override val author: Member, - repliedToMessage: Message?, - shard: Int, - val pkMessage: PKMessage, - supplier: EntitySupplier = event.kord.defaultSupplier, + event: MessageDeleteEvent, + channelId: Snowflake, + message: Message?, + guildId: Snowflake?, + override val author: Member, + repliedToMessage: Message?, + shard: Int, + val pkMessage: PKMessage, + supplier: EntitySupplier = event.kord.defaultSupplier, ) : PKMessageDeleteEvent(event, channelId, message, guildId, author, repliedToMessage, shard, supplier) { - override fun withStrategy(strategy: EntitySupplyStrategy<*>): ProxiedMessageDeleteEvent { - val strategizedEvent = event.withStrategy(strategy) - - return ProxiedMessageDeleteEvent( - strategizedEvent, - channelId, - strategizedEvent.message, - guildId, - author, - repliedToMessage, - shard, - pkMessage, - strategy.supply(kord) - ) - } - - override fun toString(): String = - "ProxiedMessageDeleteEvent(" + - "event=$event, " + - "channelId=$channelId, " + - "message=$message, " + - "guildId=$guildId, " + - "author=$author, " + - "shard=$shard, " + - "repliedToMessage=$repliedToMessage, " + - "supplier=$supplier" + - ")" + override fun withStrategy(strategy: EntitySupplyStrategy<*>): ProxiedMessageDeleteEvent { + val strategizedEvent = event.withStrategy(strategy) + + return ProxiedMessageDeleteEvent( + strategizedEvent, + channelId, + strategizedEvent.message, + guildId, + author, + repliedToMessage, + shard, + pkMessage, + strategy.supply(kord) + ) + } + + override fun toString(): String = + "ProxiedMessageDeleteEvent(" + + "event=$event, " + + "channelId=$channelId, " + + "message=$message, " + + "guildId=$guildId, " + + "author=$author, " + + "shard=$shard, " + + "repliedToMessage=$repliedToMessage, " + + "supplier=$supplier" + + ")" } /** @@ -125,65 +125,65 @@ class ProxiedMessageDeleteEvent( * receive this when a message was proxied by PluralKit, but was not cached for some reason. */ class UnProxiedMessageDeleteEvent( - event: MessageDeleteEvent, - channelId: Snowflake, - message: Message?, - guildId: Snowflake?, - author: Member?, - repliedToMessage: Message?, - shard: Int, - supplier: EntitySupplier = event.kord.defaultSupplier, + event: MessageDeleteEvent, + channelId: Snowflake, + message: Message?, + guildId: Snowflake?, + author: Member?, + repliedToMessage: Message?, + shard: Int, + supplier: EntitySupplier = event.kord.defaultSupplier, ) : PKMessageDeleteEvent(event, channelId, message, guildId, author, repliedToMessage, shard, supplier) { - override fun withStrategy(strategy: EntitySupplyStrategy<*>): UnProxiedMessageDeleteEvent { - val strategizedEvent = event.withStrategy(strategy) - - return UnProxiedMessageDeleteEvent( - strategizedEvent, - channelId, - strategizedEvent.message, - guildId, - author, - repliedToMessage, - shard, - strategy.supply(kord) - ) - } - - override fun toString(): String = - "UnProxiedMessageDeleteEvent(" + - "event=$event, " + - "channelId=$channelId, " + - "message=$message, " + - "guildId=$guildId, " + - "author=$author, " + - "repliedToMessage=$repliedToMessage, " + - "shard=$shard, " + - "supplier=$supplier" + - ")" + override fun withStrategy(strategy: EntitySupplyStrategy<*>): UnProxiedMessageDeleteEvent { + val strategizedEvent = event.withStrategy(strategy) + + return UnProxiedMessageDeleteEvent( + strategizedEvent, + channelId, + strategizedEvent.message, + guildId, + author, + repliedToMessage, + shard, + strategy.supply(kord) + ) + } + + override fun toString(): String = + "UnProxiedMessageDeleteEvent(" + + "event=$event, " + + "channelId=$channelId, " + + "message=$message, " + + "guildId=$guildId, " + + "author=$author, " + + "repliedToMessage=$repliedToMessage, " + + "shard=$shard, " + + "supplier=$supplier" + + ")" } internal suspend fun MessageDeleteEvent.proxied(p: PKMessage, referencedMessage: Message?): ProxiedMessageDeleteEvent { - val member = getGuildOrNull()!!.getMemberOrNull(p.sender)!! - - return ProxiedMessageDeleteEvent( - this, - channelId, - message, - guildId, - member, - referencedMessage, - shard, - p, - ) + val member = getGuildOrNull()!!.getMemberOrNull(p.sender)!! + + return ProxiedMessageDeleteEvent( + this, + channelId, + message, + guildId, + member, + referencedMessage, + shard, + p, + ) } internal suspend fun MessageDeleteEvent.unproxied(): UnProxiedMessageDeleteEvent = - UnProxiedMessageDeleteEvent( - this, - channelId, - message, - guildId, - message?.getAuthorAsMemberOrNull(), - message?.referencedMessage, - shard - ) + UnProxiedMessageDeleteEvent( + this, + channelId, + message, + guildId, + message?.getAuthorAsMemberOrNull(), + message?.referencedMessage, + shard + ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageUpdateEvent.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageUpdateEvent.kt index 70bee8ede5..24fab48007 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageUpdateEvent.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/events/PKMessageUpdateEvent.kt @@ -42,42 +42,42 @@ import dev.kord.core.supplier.EntitySupplyStrategy * @property repliedToMessage The original message that was replied to, even if this one was proxied. */ abstract class PKMessageUpdateEvent( - open val event: MessageUpdateEvent, - open val channelId: Snowflake, - open val messageId: Snowflake, - open val old: Message?, - open val new: DiscordPartialMessage, - open val author: Member?, - open val repliedToMessage: Message?, - override val shard: Int, - override val supplier: EntitySupplier = event.kord.defaultSupplier, + open val event: MessageUpdateEvent, + open val channelId: Snowflake, + open val messageId: Snowflake, + open val old: Message?, + open val new: DiscordPartialMessage, + open val author: Member?, + open val repliedToMessage: Message?, + override val shard: Int, + override val supplier: EntitySupplier = event.kord.defaultSupplier, ) : KordExEvent, Strategizable, ChannelEvent, MessageEvent, MemberEvent { - /** @suppress Forwards to [repliedToMessage], **/ - val referencedMessage get() = repliedToMessage + /** @suppress Forwards to [repliedToMessage], **/ + val referencedMessage get() = repliedToMessage - override val channel: MessageChannelBehavior get() = kord.unsafe.messageChannel(channelId) - override val guild: GuildBehavior? get() = member?.guild - override val member: MemberBehavior? get() = author - override val message: MessageBehavior get() = kord.unsafe.message(messageId = messageId, channelId = channelId) - override val user: UserBehavior? get() = author + override val channel: MessageChannelBehavior get() = kord.unsafe.messageChannel(channelId) + override val guild: GuildBehavior? get() = member?.guild + override val member: MemberBehavior? get() = author + override val message: MessageBehavior get() = kord.unsafe.message(messageId = messageId, channelId = channelId) + override val user: UserBehavior? get() = author - override suspend fun getChannel(): Channel = channel.asChannel() - override suspend fun getChannelOrNull(): Channel = channel.asChannel() + override suspend fun getChannel(): Channel = channel.asChannel() + override suspend fun getChannelOrNull(): Channel = channel.asChannel() - override suspend fun getGuild(): Guild = getGuildOrNull()!! - override suspend fun getGuildOrNull(): Guild? = member?.guild?.asGuildOrNull() + override suspend fun getGuild(): Guild = getGuildOrNull()!! + override suspend fun getGuildOrNull(): Guild? = member?.guild?.asGuildOrNull() - override suspend fun getMember(): Member = author!! - override suspend fun getMemberOrNull(): Member? = author + override suspend fun getMember(): Member = author!! + override suspend fun getMemberOrNull(): Member? = author - override suspend fun getMessage(): Message = - supplier.getMessage(channelId = channelId, messageId = messageId) + override suspend fun getMessage(): Message = + supplier.getMessage(channelId = channelId, messageId = messageId) - override suspend fun getMessageOrNull(): Message? = - supplier.getMessageOrNull(channelId = channelId, messageId = messageId) + override suspend fun getMessageOrNull(): Message? = + supplier.getMessageOrNull(channelId = channelId, messageId = messageId) - override suspend fun getUser(): User = author!! - override suspend fun getUserOrNull(): User? = author + override suspend fun getUser(): User = author!! + override suspend fun getUserOrNull(): User? = author } /** @@ -86,114 +86,114 @@ abstract class PKMessageUpdateEvent( * @property pkMessage The PluralKit message object with metadata about the proxied message. */ class ProxiedMessageUpdateEvent( - event: MessageUpdateEvent, - channelId: Snowflake, - messageId: Snowflake, - old: Message?, - new: DiscordPartialMessage, - override val author: Member, - repliedToMessage: Message?, - shard: Int, - val pkMessage: PKMessage, - supplier: EntitySupplier = event.kord.defaultSupplier, + event: MessageUpdateEvent, + channelId: Snowflake, + messageId: Snowflake, + old: Message?, + new: DiscordPartialMessage, + override val author: Member, + repliedToMessage: Message?, + shard: Int, + val pkMessage: PKMessage, + supplier: EntitySupplier = event.kord.defaultSupplier, ) : PKMessageUpdateEvent(event, channelId, messageId, old, new, author, repliedToMessage, shard, supplier) { - override fun withStrategy(strategy: EntitySupplyStrategy<*>): ProxiedMessageUpdateEvent { - val strategizedEvent = event.withStrategy(strategy) - - return ProxiedMessageUpdateEvent( - strategizedEvent, - channelId, - messageId, - strategizedEvent.old, - strategizedEvent.new, - author, - repliedToMessage, - shard, - pkMessage, - strategy.supply(kord) - ) - } - - override fun toString(): String = - "ProxiedMessageUpdateEvent(" + - "event=$event, " + - "channelId=$channelId, " + - "messageId=$messageId, " + - "old=$old, " + - "new=$new, " + - "member=$author, " + - "shard=$shard, " + - "supplier=$supplier" + - ")" + override fun withStrategy(strategy: EntitySupplyStrategy<*>): ProxiedMessageUpdateEvent { + val strategizedEvent = event.withStrategy(strategy) + + return ProxiedMessageUpdateEvent( + strategizedEvent, + channelId, + messageId, + strategizedEvent.old, + strategizedEvent.new, + author, + repliedToMessage, + shard, + pkMessage, + strategy.supply(kord) + ) + } + + override fun toString(): String = + "ProxiedMessageUpdateEvent(" + + "event=$event, " + + "channelId=$channelId, " + + "messageId=$messageId, " + + "old=$old, " + + "new=$new, " + + "member=$author, " + + "shard=$shard, " + + "supplier=$supplier" + + ")" } /** * A [MessageUpdateEvent] wrapper that represents a message that was **not** proxied by PluralKit. */ class UnProxiedMessageUpdateEvent( - event: MessageUpdateEvent, - channelId: Snowflake, - messageId: Snowflake, - old: Message?, - new: DiscordPartialMessage, - author: Member?, - repliedToMessage: Message?, - shard: Int, - supplier: EntitySupplier = event.kord.defaultSupplier, + event: MessageUpdateEvent, + channelId: Snowflake, + messageId: Snowflake, + old: Message?, + new: DiscordPartialMessage, + author: Member?, + repliedToMessage: Message?, + shard: Int, + supplier: EntitySupplier = event.kord.defaultSupplier, ) : PKMessageUpdateEvent(event, channelId, messageId, old, new, author, repliedToMessage, shard, supplier) { - override fun withStrategy(strategy: EntitySupplyStrategy<*>): UnProxiedMessageUpdateEvent { - val strategizedEvent = event.withStrategy(strategy) - - return UnProxiedMessageUpdateEvent( - strategizedEvent, - channelId, - messageId, - strategizedEvent.old, - strategizedEvent.new, - author, - repliedToMessage, - shard, - strategy.supply(kord) - ) - } - - override fun toString(): String = - "UnProxiedMessageUpdateEvent(" + - "event=$event, " + - "channelId=$channelId, " + - "messageId=$messageId, " + - "old=$old, " + - "new=$new, " + - "member=$author, " + - "shard=$shard, " + - "supplier=$supplier" + - ")" + override fun withStrategy(strategy: EntitySupplyStrategy<*>): UnProxiedMessageUpdateEvent { + val strategizedEvent = event.withStrategy(strategy) + + return UnProxiedMessageUpdateEvent( + strategizedEvent, + channelId, + messageId, + strategizedEvent.old, + strategizedEvent.new, + author, + repliedToMessage, + shard, + strategy.supply(kord) + ) + } + + override fun toString(): String = + "UnProxiedMessageUpdateEvent(" + + "event=$event, " + + "channelId=$channelId, " + + "messageId=$messageId, " + + "old=$old, " + + "new=$new, " + + "member=$author, " + + "shard=$shard, " + + "supplier=$supplier" + + ")" } internal suspend fun MessageUpdateEvent.proxied(p: PKMessage, referencedMessage: Message?): ProxiedMessageUpdateEvent { - val member = kord.getChannelOf(channelId)!!.getGuild().getMemberOrNull(p.sender)!! - - return ProxiedMessageUpdateEvent( - this, - channelId, - messageId, - old, - new, - member, - referencedMessage, - shard, - p, - ) + val member = kord.getChannelOf(channelId)!!.getGuild().getMemberOrNull(p.sender)!! + + return ProxiedMessageUpdateEvent( + this, + channelId, + messageId, + old, + new, + member, + referencedMessage, + shard, + p, + ) } internal suspend fun MessageUpdateEvent.unproxied(): UnProxiedMessageUpdateEvent = - UnProxiedMessageUpdateEvent( - this, - channelId, - messageId, - old, - new, - message.asMessageOrNull()?.getAuthorAsMemberOrNull(), - message.asMessageOrNull()?.referencedMessage, - shard, - ) + UnProxiedMessageUpdateEvent( + this, + channelId, + messageId, + old, + new, + message.asMessageOrNull()?.getAuthorAsMemberOrNull(), + message.asMessageOrNull()?.referencedMessage, + shard, + ) diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/storage/PKGuildConfig.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/storage/PKGuildConfig.kt index f5619bf840..02f075c27f 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/storage/PKGuildConfig.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/storage/PKGuildConfig.kt @@ -5,10 +5,10 @@ */ @file:Suppress( - "UnderscoresInNumericLiterals", - "UndocumentedPublicClass", - "UndocumentedPublicFunction", - "UndocumentedPublicProperty", + "UnderscoresInNumericLiterals", + "UndocumentedPublicClass", + "UndocumentedPublicFunction", + "UndocumentedPublicProperty", ) package com.kotlindiscord.kord.extensions.modules.extra.pluralkit.storage @@ -21,18 +21,18 @@ import net.peanuuutz.tomlkt.TomlComment @Serializable @Suppress("DataClassShouldBeImmutable", "MagicNumber") data class PKGuildConfig( - @TomlComment( - "Base URL to use when attempting to hit the PluralKit API, without the /vX used to specify the version." - ) - var apiUrl: String = "https://api.pluralkit.me", + @TomlComment( + "Base URL to use when attempting to hit the PluralKit API, without the /vX used to specify the version." + ) + var apiUrl: String = "https://api.pluralkit.me", - @TomlComment( - "The ID of the PluralKit instance to use, if not the default instance." - ) - var botId: Snowflake = Snowflake(466378653216014359), + @TomlComment( + "The ID of the PluralKit instance to use, if not the default instance." + ) + var botId: Snowflake = Snowflake(466378653216014359), - @TomlComment( - "Whether PluralKit integration should be enabled on this server." - ) - var enabled: Boolean = true, + @TomlComment( + "Whether PluralKit integration should be enabled on this server." + ) + var enabled: Boolean = true, ) : Data diff --git a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/utils/LRUHashMap.kt b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/utils/LRUHashMap.kt index 9d3f957000..a9acf15e93 100644 --- a/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/utils/LRUHashMap.kt +++ b/extra-modules/extra-pluralkit/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/pluralkit/utils/LRUHashMap.kt @@ -11,6 +11,6 @@ package com.kotlindiscord.kord.extensions.modules.extra.pluralkit.utils * max size. */ class LRUHashMap(private val maxSize: Int) : LinkedHashMap() { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = - size > maxSize + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = + size > maxSize } diff --git a/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit.properties b/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit.properties index 536adea1bb..c8f0076619 100644 --- a/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit.properties +++ b/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit.properties @@ -3,39 +3,30 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argument.api-url.name=api-url argument.api-url.description=Set an alternative API url, or "reset" to use the default - argument.bot.name=bot argument.bot.description=Select an alternative PK instance, if needed - argument.toggle.name=enable argument.toggle.description=Set whether the PK integration should be used on this server - arguments.reset=reset - command.pluralkit.name=pluralkit command.pluralkit.description=Manage and view the PluralKit integration settings for this server - command.pluralkit.api-url.name=api-url command.pluralkit.api-url.description=Set a custom API URL, "reset" to reset command.pluralkit.api-url.response.current=**Current API URL:** `{0}` command.pluralkit.api-url.response.reset=**API URL reset to default:** `{0}` command.pluralkit.api-url.response.updated=**API URL updated:** `{0}` - command.pluralkit.bot.name=bot command.pluralkit.bot.description=Pick your custom PluralKit instance, if you have one command.pluralkit.bot.response.current=**Current PK instance:** {0} command.pluralkit.bot.response.updated=**PK instance updated:** {0} - command.pluralkit.status.name=status command.pluralkit.status.description=Check the settings for this server's PluralKit integration command.pluralkit.status.response.title=PluralKit Integration Settings command.pluralkit.status.response.description=**API URL:** `{0}`\n\ **PK Instance:**{1}\n\ **Enabled:**{2} - command.pluralkit.toggle-support.name=toggle command.pluralkit.toggle-support.description=Disable or enable the PluralKit integration as required command.pluralkit.toggle-support.response.current=**Enabled:** {0} diff --git a/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit_en_GB.properties b/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit_en_GB.properties index 1dee9412b8..7e3e27f4bb 100644 --- a/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit_en_GB.properties +++ b/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit_en_GB.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - arguments.reset=reset command.pluralkit.name=pluralkit command.pluralkit.description=Manage and view the PluralKit integration settings for this server diff --git a/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit_pt.properties b/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit_pt.properties index 128beb071d..39691b0e57 100644 --- a/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit_pt.properties +++ b/extra-modules/extra-pluralkit/src/main/resources/translations/kordex/pluralkit_pt.properties @@ -1,5 +1,3 @@ - - argument.bot.name=bot argument.bot.description=Seleciona uma instância de PK alternativa, se necessário command.pluralkit.description=Gere e vê as definições da integração de PluralKit neste servidor diff --git a/extra-modules/extra-tags/build.gradle.kts b/extra-modules/extra-tags/build.gradle.kts index 869e6c3ec9..7b9995343d 100644 --- a/extra-modules/extra-tags/build.gradle.kts +++ b/extra-modules/extra-tags/build.gradle.kts @@ -1,33 +1,33 @@ plugins { - `kordex-module` - `published-module` - `disable-explicit-api-mode` + `kordex-module` + `published-module` + `disable-explicit-api-mode` - kotlin("plugin.serialization") + kotlin("plugin.serialization") } metadata { - name = "KordEx Extra: Tags" - description = "KordEx extra module that provides a set of commands for storing and retrieving tagged text snippets" + name = "KordEx Extra: Tags" + description = "KordEx extra module that provides a set of commands for storing and retrieving tagged text snippets" } repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } } dependencies { - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - implementation(libs.bundles.logging) - implementation(libs.kotlin.stdlib) - implementation(libs.ktor.logging) + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) + implementation(libs.ktor.logging) - implementation(project(":kord-extensions")) - implementation(project(":modules:unsafe")) + implementation(project(":kord-extensions")) + implementation(project(":modules:unsafe")) } group = "com.kotlindiscord.kord.extensions" diff --git a/extra-modules/extra-welcome/build.gradle.kts b/extra-modules/extra-welcome/build.gradle.kts index 8e5ad94dbf..c7044263ad 100644 --- a/extra-modules/extra-welcome/build.gradle.kts +++ b/extra-modules/extra-welcome/build.gradle.kts @@ -1,34 +1,34 @@ plugins { - `kordex-module` - `published-module` - `disable-explicit-api-mode` + `kordex-module` + `published-module` + `disable-explicit-api-mode` - kotlin("plugin.serialization") + kotlin("plugin.serialization") } metadata { - name = "KordEx Extra: Welcome" - description = "KordEx extra module that provides welcome channel management driven by web-accessible YAML files" + name = "KordEx Extra: Welcome" + description = "KordEx extra module that provides welcome channel management driven by web-accessible YAML files" } repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } } dependencies { - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - implementation(libs.bundles.logging) - implementation(libs.kotlin.stdlib) - implementation(libs.ktor.logging) + implementation(libs.bundles.logging) + implementation(libs.kotlin.stdlib) + implementation(libs.ktor.logging) - implementation(libs.kaml) + implementation(libs.kaml) - implementation(project(":kord-extensions")) + implementation(project(":kord-extensions")) } group = "com.kotlindiscord.kord.extensions" diff --git a/gradle.properties b/gradle.properties index bf4a8ae5a5..4bc07a6a22 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,6 @@ -org.gradle.parallel = true -kotlin.incremental = true - -ksp.incremental = false - -projectVersion = 1.8.0-SNAPSHOT - +org.gradle.parallel=true +kotlin.incremental=true +ksp.incremental=false +projectVersion=1.8.0-SNAPSHOT #dokka will run out of memory with the default meta space org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 726ccf9f39..53a4bc800f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ kord = { module = "dev.kord:kord-core-voice", version.ref = "kord" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } -kx-coro = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kx-coro"} +kx-coro = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kx-coro" } kx-ser = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kx-ser" } kx-ser-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kx-ser" } linkie = { module = "me.shedaniel:linkie-core", version.ref = "linkie" } @@ -71,5 +71,5 @@ time4j-tzdata = { module = "net.time4j:time4j-tzdata", version.ref = "time4j-tzd toml = { module = "net.peanuuutz:tomlkt", version.ref = "toml" } [bundles] -commons = [ "commons-validator" ] -logging = [ "kotlin-logging", "slf4j" ] +commons = ["commons-validator"] +logging = ["kotlin-logging", "slf4j"] diff --git a/kord-extensions/build.gradle.kts b/kord-extensions/build.gradle.kts index 5e9ba36437..2420ef8901 100644 --- a/kord-extensions/build.gradle.kts +++ b/kord-extensions/build.gradle.kts @@ -1,67 +1,67 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { - repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } - } + repositories { + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } } plugins { - `kordex-module` - `published-module` - `tested-module` - `ksp-module` + `kordex-module` + `published-module` + `tested-module` + `ksp-module` } metadata { - name = "KordEx Core" - description = "Core Kord Extensions module, providing everything you need to write a bot with KordEx" + name = "KordEx Core" + description = "Core Kord Extensions module, providing everything you need to write a bot with KordEx" } dependencies { - api(libs.icu4j) // For translations - api(libs.koin.core) - api(libs.koin.logger) + api(libs.icu4j) // For translations + api(libs.koin.core) + api(libs.koin.logger) - api(libs.kord) + api(libs.kord) - api(libs.bundles.logging) // Basic logging setup - api(libs.jemoji) // Basic logging setup - api(libs.kx.ser) - api(libs.sentry) // Needs to be transitive or bots will start breaking - api(libs.toml) - api(libs.pf4j) + api(libs.bundles.logging) // Basic logging setup + api(libs.jemoji) // Basic logging setup + api(libs.kx.ser) + api(libs.sentry) // Needs to be transitive or bots will start breaking + api(libs.toml) + api(libs.pf4j) - api(project(":annotations")) - api(project(":token-parser")) + api(project(":annotations")) + api(project(":token-parser")) - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - implementation(libs.bundles.commons) - implementation(libs.kotlin.stdlib) + implementation(libs.bundles.commons) + implementation(libs.kotlin.stdlib) - testImplementation(libs.groovy) // For logback config - testImplementation(libs.jansi) - testImplementation(libs.junit) - testImplementation(libs.koin.test) - testImplementation(libs.logback) - testImplementation(libs.logback.groovy) + testImplementation(libs.groovy) // For logback config + testImplementation(libs.jansi) + testImplementation(libs.junit) + testImplementation(libs.koin.test) + testImplementation(libs.logback) + testImplementation(libs.logback.groovy) - ksp(project(":annotation-processor")) - kspTest(project(":annotation-processor")) + ksp(project(":annotation-processor")) + kspTest(project(":annotation-processor")) } val compileKotlin: KotlinCompile by tasks compileKotlin.kotlinOptions { - languageVersion = "1.7" + languageVersion = "1.7" } dokkaModule { moduleName = "Kord Extensions" - includes.add("packages.md") + includes.add("packages.md") } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/ExtensibleBot.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/ExtensibleBot.kt index 6a725f73c9..b3cec9b7ea 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/ExtensibleBot.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/ExtensibleBot.kt @@ -64,439 +64,439 @@ import org.koin.dsl.bind * @param token Token for connecting to Discord. */ public open class ExtensibleBot( - public val settings: ExtensibleBotBuilder, - private val token: String, + public val settings: ExtensibleBotBuilder, + private val token: String, ) : KordExKoinComponent, Lockable { - override var mutex: Mutex? = Mutex() - override var locking: Boolean = settings.membersBuilder.lockMemberRequests - - /** @suppress Meant for internal use by public inline function. **/ - public val kordRef: Kord by inject() - - /** - * A list of all registered event handlers. - */ - public open val eventHandlers: MutableList> = mutableListOf() - - /** - * A map of the names of all loaded [Extension]s to their instances. - */ - public open val extensions: MutableStringKeyedMap = mutableMapOf() - - /** @suppress **/ - public open val eventPublisher: MutableSharedFlow = MutableSharedFlow() - - /** A [Flow] representing a combined set of Kord events and Kord Extensions events. **/ - public open val events: SharedFlow = eventPublisher.asSharedFlow() - - /** @suppress **/ - public open var initialized: Boolean = false - - /** @suppress **/ - public open val logger: KLogger = KotlinLogging.logger {} - - /** @suppress Function that sets up the bot early on, called by the builder. **/ - public open suspend fun setup() { - val kord = settings.kordBuilder(token) { - cache { - settings.cacheBuilder.builder.invoke(this, it) - } - - defaultStrategy = settings.cacheBuilder.defaultStrategy - - if (settings.shardingBuilder != null) { - sharding(settings.shardingBuilder!!) - } - - enableShutdownHook = settings.hooksBuilder.kordShutdownHook - - settings.kordHooks.forEach { it() } - - gatewayEventInterceptor = DefaultGatewayEventInterceptor( - customContextCreator = { _, _ -> - mutableMapOf() - } - ) - } - - loadModule { single { kord } bind Kord::class } - - settings.cacheBuilder.dataCacheBuilder.invoke(kord, kord.cache) - - kord.on { - kord.launch { - send(this@on) - } - } - - addDefaultExtensions() - } - - /** Start up the bot and log into Discord. **/ - public open suspend fun start() { - settings.hooksBuilder.runBeforeStart(this) - - if (!initialized) registerListeners() - - getKoin().get().login { - this.presence(settings.presenceBuilder) - this.intents = Intents(settings.intentsBuilder!!) - } - } - - /** - * Stop the bot by logging out [Kord]. - * - * This will leave the Koin context intact, so subsequent restarting of the bot is possible. - * - * @see close - **/ - public open suspend fun stop() { - getKoin().get().logout() - } - - /** - * Stop the bot by shutting down [Kord] and removing its Koin context. - * - * Restarting the bot after closing will result in undefined behavior - * because the Koin context needed to start will no longer exist. - * - * If a bot has been closed, then it must be fully rebuilt to start again. - * - * If a new bot is going to be built, then the previous bot must be closed first. - * - * @see stop - **/ - public open suspend fun close() { - getKoin().get().shutdown() - KordExContext.stopKoin() - } - - /** Start up the bot and log into Discord, but launched via Kord's coroutine scope. **/ - public open fun startAsync(): Job = - getKoin().get().launch { - start() - } - - /** This function sets up all of the bot's default event listeners. **/ - public open suspend fun registerListeners() { - val eventJson = Json { - ignoreUnknownKeys = true - } - - on { - withLock { // If configured, this won't be concurrent, saving larger bots from spammy rate limits - if ( - settings.membersBuilder.guildsToFill == null || - settings.membersBuilder.guildsToFill!!.contains(guild.id) - ) { - logger.debug { "Requesting members for guild: ${guild.name}" } - - guild.requestMembers { - presences = settings.membersBuilder.fillPresences - requestAllMembers() - }.collect() - } - } - } - - @Suppress("TooGenericExceptionCaught") - on { - try { - val eventObj = when (name) { - "GUILD_JOIN_REQUEST_DELETE" -> { - val data: GuildJoinRequestDelete = eventJson.decodeFromJsonElement(this.data!!) - - GuildJoinRequestDeleteEvent(data) - } - - "GUILD_JOIN_REQUEST_UPDATE" -> { - val data: GuildJoinRequestUpdate = eventJson.decodeFromJsonElement(this.data!!) - - GuildJoinRequestUpdateEvent(data) - } - - else -> null - } ?: return@on - - send(eventObj) - } catch (e: Exception) { - logger.error(e) { "Failed to deserialize event: $data" } - } - } - - on { - logger.warn { "Disconnected: $closeCode" } - } - - on { - getKoin().get().handle(this) - } - - on { - getKoin().get().handle(this) - } - - on { - getKoin().get().handle(this) - } - - if (settings.chatCommandsBuilder.enabled) { - on { - getKoin().get().handleEvent(this) - } - } else { - logger.debug { - "Chat command support is disabled - set `enabled` to `true` in the `chatCommands` builder" + - " if you want to use them." - } - } - - if (settings.applicationCommandsBuilder.enabled) { - on { - getKoin().get().handle(this) - } - - on { - getKoin().get().handle(this) - } - - on { - getKoin().get().handle(this) - } - - on { - getKoin().get().handle(this) - } - - getKoin().get().initialRegistration() - } else { - logger.debug { - "Application command support is disabled - set `enabled` to `true` in the " + - "`applicationCommands` builder if you want to use them." - } - } - - if (!initialized) { - eventHandlers.forEach { handler -> - handler.listenerRegistrationCallable?.invoke() ?: logger.error { - "Event handler $handler does not have a listener registration callback. This should never happen!" - } - } - - initialized = true - } - } - - /** This function adds all of the default extensions when the bot is being set up. **/ - public open suspend fun addDefaultExtensions() { - val extBuilder = settings.extensionsBuilder - - if (extBuilder.helpExtensionBuilder.enableBundledExtension) { - this.addExtension(::HelpExtension) - } - - if (extBuilder.sentryExtensionBuilder.enable && extBuilder.sentryExtensionBuilder.feedbackExtension) { - this.addExtension(::SentryExtension) - } - } - - /** - * Subscribe to an event. You shouldn't need to use this directly, but it's here just in case. - * - * You can subscribe to any type, realistically - but this is intended to be used only with Kord - * [Event] subclasses, and our own [KordExEvent]s. - * - * @param T Types of event to subscribe to. - * @param scope Coroutine scope to run the body of your callback under. - * @param consumer The callback to run when the event is fired. - */ - public inline fun on( - launch: Boolean = true, - scope: CoroutineScope = this.getKoin().get(), - noinline consumer: suspend T.() -> Unit, - ): Job = - events.buffer(Channel.UNLIMITED) - .filterIsInstance() - .onEach { - runCatching { - if (launch) kordRef.launch { consumer(it) } else consumer(it) - }.onFailure { logger.catching(it) } - }.catch { logger.catching(it) } - .launchIn(scope) - - /** - * @suppress - */ - public suspend inline fun send(event: Event) { - eventPublisher.emit(event) - } - - /** - * Install an [Extension] to this bot. - * - * This function will call the given builder function and store the resulting extension object, ready to be - * set up when the next [ReadyEvent] happens. - * - * @param builder Builder function (or extension constructor) that takes an [ExtensibleBot] instance and - * returns an [Extension]. - */ - @Throws(InvalidExtensionException::class) - public open suspend fun addExtension(builder: () -> Extension) { - val extensionObj = builder.invoke() - - if (extensions.contains(extensionObj.name)) { - logger.error { - "Extension with duplicate name ${extensionObj.name} loaded - unloading previously registered extension" - } - - unloadExtension(extensionObj.name) - } - - extensions[extensionObj.name] = extensionObj - loadExtension(extensionObj.name) - - if (!extensionObj.loaded) { - logger.warn { "Failed to set up extension: ${extensionObj.name}" } - } else { - logger.debug { "Loaded extension: ${extensionObj.name}" } - - settings.hooksBuilder.runExtensionAdded(this, extensionObj) - } - } - - /** - * Reload an unloaded [Extension] from this bot, by name. - * - * This function **does not** create a new extension object - it simply - * calls its `setup()` function. Loaded extensions can - * be unload again by calling [unloadExtension]. - * - * This function simply returns if the extension isn't found. - * - * @param extension The name of the [Extension] to unload. - */ - @Throws(InvalidExtensionException::class) - public open suspend fun loadExtension(extension: String) { - val extensionObj = extensions[extension] ?: return - - if (!extensionObj.loaded) { - extensionObj.doSetup() - } - } - - /** - * Find the first loaded extension that is an instance of the type provided in `T`. - * - * This can be used to find an extension based on, for example, an implemented interface. - * - * @param T Types to match extensions against. - */ - public inline fun findExtension(): T? = - findExtensions().firstOrNull() - - /** - * Find all loaded extensions that are instances of the type provided in `T`. - * - * This can be used to find extensions based on, for example, an implemented interface. - * - * @param T Types to match extensions against. - */ - public inline fun findExtensions(): List = - extensions.values.filterIsInstance() - - /** - * Unload an installed [Extension] from this bot, by name. - * - * This function **does not** remove the extension object - it simply - * removes its event handlers and commands. Unloaded extensions can - * be loaded again by calling [loadExtension]. - * - * This function simply returns if the extension isn't found. - * - * @param extension The name of the [Extension] to unload. - */ - public open suspend fun unloadExtension(extension: String) { - val extensionObj = extensions[extension] ?: return - - if (extensionObj.loaded) { - extensionObj.doUnload() - } - } - - /** - * Remove an installed [Extension] from this bot, by name. - * - * This function will unload the given extension (if it's loaded), and remove the - * extension object from the list of registered extensions. - * - * @param extension The name of the [Extension] to unload. - * - * @suppress This is meant to be used with the module system, and isn't necessarily a user-facing API. - * You need to be quite careful with this! - */ - public open suspend fun removeExtension(extension: String) { - unloadExtension(extension) - - extensions.remove(extension) - } - - /** - * Directly register an [EventHandler] to this bot. - * - * Generally speaking, you shouldn't call this directly - instead, create an [Extension] and - * call the [Extension.event] function in your [Extension.setup] function. - * - * This function will throw an [EventHandlerRegistrationException] if the event handler has already been registered. - * - * @param handler The event handler to be registered. - * @throws EventHandlerRegistrationException Thrown if the event handler could not be registered. - */ - @Throws(EventHandlerRegistrationException::class) - public inline fun addEventHandler(handler: EventHandler) { - if (eventHandlers.contains(handler)) { - throw EventHandlerRegistrationException( - "Event handler already registered in '${handler.extension.name}' extension." - ) - } - - if (initialized) { - handler.listenerRegistrationCallable?.invoke() ?: error( - "Event handler $handler does not have a listener registration callback. This should never happen!" - ) - } - - eventHandlers.add(handler) - } - - /** - * Directly register an [EventHandler] to this bot. - * - * Generally speaking, you shouldn't call this directly - instead, create an [Extension] and - * call the [Extension.event] function in your [Extension.setup] function. - * - * This function will throw an [EventHandlerRegistrationException] if the event handler has already been registered. - * - * @param handler The event handler to be registered. - * @throws EventHandlerRegistrationException Thrown if the event handler could not be registered. - */ - @Throws(EventHandlerRegistrationException::class) - public inline fun registerListenerForHandler(handler: EventHandler): Job { - return on { - handler.call(this) - } - } - - /** - * Directly remove a registered [EventHandler] from this bot. - * - * This function is used when extensions are unloaded, in order to clear out their event handlers. - * No exception is thrown if the event handler wasn't registered. - * - * @param handler The event handler to be removed. - */ - public open fun removeEventHandler(handler: EventHandler): Boolean = eventHandlers.remove(handler) + override var mutex: Mutex? = Mutex() + override var locking: Boolean = settings.membersBuilder.lockMemberRequests + + /** @suppress Meant for internal use by public inline function. **/ + public val kordRef: Kord by inject() + + /** + * A list of all registered event handlers. + */ + public open val eventHandlers: MutableList> = mutableListOf() + + /** + * A map of the names of all loaded [Extension]s to their instances. + */ + public open val extensions: MutableStringKeyedMap = mutableMapOf() + + /** @suppress **/ + public open val eventPublisher: MutableSharedFlow = MutableSharedFlow() + + /** A [Flow] representing a combined set of Kord events and Kord Extensions events. **/ + public open val events: SharedFlow = eventPublisher.asSharedFlow() + + /** @suppress **/ + public open var initialized: Boolean = false + + /** @suppress **/ + public open val logger: KLogger = KotlinLogging.logger {} + + /** @suppress Function that sets up the bot early on, called by the builder. **/ + public open suspend fun setup() { + val kord = settings.kordBuilder(token) { + cache { + settings.cacheBuilder.builder.invoke(this, it) + } + + defaultStrategy = settings.cacheBuilder.defaultStrategy + + if (settings.shardingBuilder != null) { + sharding(settings.shardingBuilder!!) + } + + enableShutdownHook = settings.hooksBuilder.kordShutdownHook + + settings.kordHooks.forEach { it() } + + gatewayEventInterceptor = DefaultGatewayEventInterceptor( + customContextCreator = { _, _ -> + mutableMapOf() + } + ) + } + + loadModule { single { kord } bind Kord::class } + + settings.cacheBuilder.dataCacheBuilder.invoke(kord, kord.cache) + + kord.on { + kord.launch { + send(this@on) + } + } + + addDefaultExtensions() + } + + /** Start up the bot and log into Discord. **/ + public open suspend fun start() { + settings.hooksBuilder.runBeforeStart(this) + + if (!initialized) registerListeners() + + getKoin().get().login { + this.presence(settings.presenceBuilder) + this.intents = Intents(settings.intentsBuilder!!) + } + } + + /** + * Stop the bot by logging out [Kord]. + * + * This will leave the Koin context intact, so subsequent restarting of the bot is possible. + * + * @see close + **/ + public open suspend fun stop() { + getKoin().get().logout() + } + + /** + * Stop the bot by shutting down [Kord] and removing its Koin context. + * + * Restarting the bot after closing will result in undefined behavior + * because the Koin context needed to start will no longer exist. + * + * If a bot has been closed, then it must be fully rebuilt to start again. + * + * If a new bot is going to be built, then the previous bot must be closed first. + * + * @see stop + **/ + public open suspend fun close() { + getKoin().get().shutdown() + KordExContext.stopKoin() + } + + /** Start up the bot and log into Discord, but launched via Kord's coroutine scope. **/ + public open fun startAsync(): Job = + getKoin().get().launch { + start() + } + + /** This function sets up all of the bot's default event listeners. **/ + public open suspend fun registerListeners() { + val eventJson = Json { + ignoreUnknownKeys = true + } + + on { + withLock { // If configured, this won't be concurrent, saving larger bots from spammy rate limits + if ( + settings.membersBuilder.guildsToFill == null || + settings.membersBuilder.guildsToFill!!.contains(guild.id) + ) { + logger.debug { "Requesting members for guild: ${guild.name}" } + + guild.requestMembers { + presences = settings.membersBuilder.fillPresences + requestAllMembers() + }.collect() + } + } + } + + @Suppress("TooGenericExceptionCaught") + on { + try { + val eventObj = when (name) { + "GUILD_JOIN_REQUEST_DELETE" -> { + val data: GuildJoinRequestDelete = eventJson.decodeFromJsonElement(this.data!!) + + GuildJoinRequestDeleteEvent(data) + } + + "GUILD_JOIN_REQUEST_UPDATE" -> { + val data: GuildJoinRequestUpdate = eventJson.decodeFromJsonElement(this.data!!) + + GuildJoinRequestUpdateEvent(data) + } + + else -> null + } ?: return@on + + send(eventObj) + } catch (e: Exception) { + logger.error(e) { "Failed to deserialize event: $data" } + } + } + + on { + logger.warn { "Disconnected: $closeCode" } + } + + on { + getKoin().get().handle(this) + } + + on { + getKoin().get().handle(this) + } + + on { + getKoin().get().handle(this) + } + + if (settings.chatCommandsBuilder.enabled) { + on { + getKoin().get().handleEvent(this) + } + } else { + logger.debug { + "Chat command support is disabled - set `enabled` to `true` in the `chatCommands` builder" + + " if you want to use them." + } + } + + if (settings.applicationCommandsBuilder.enabled) { + on { + getKoin().get().handle(this) + } + + on { + getKoin().get().handle(this) + } + + on { + getKoin().get().handle(this) + } + + on { + getKoin().get().handle(this) + } + + getKoin().get().initialRegistration() + } else { + logger.debug { + "Application command support is disabled - set `enabled` to `true` in the " + + "`applicationCommands` builder if you want to use them." + } + } + + if (!initialized) { + eventHandlers.forEach { handler -> + handler.listenerRegistrationCallable?.invoke() ?: logger.error { + "Event handler $handler does not have a listener registration callback. This should never happen!" + } + } + + initialized = true + } + } + + /** This function adds all of the default extensions when the bot is being set up. **/ + public open suspend fun addDefaultExtensions() { + val extBuilder = settings.extensionsBuilder + + if (extBuilder.helpExtensionBuilder.enableBundledExtension) { + this.addExtension(::HelpExtension) + } + + if (extBuilder.sentryExtensionBuilder.enable && extBuilder.sentryExtensionBuilder.feedbackExtension) { + this.addExtension(::SentryExtension) + } + } + + /** + * Subscribe to an event. You shouldn't need to use this directly, but it's here just in case. + * + * You can subscribe to any type, realistically - but this is intended to be used only with Kord + * [Event] subclasses, and our own [KordExEvent]s. + * + * @param T Types of event to subscribe to. + * @param scope Coroutine scope to run the body of your callback under. + * @param consumer The callback to run when the event is fired. + */ + public inline fun on( + launch: Boolean = true, + scope: CoroutineScope = this.getKoin().get(), + noinline consumer: suspend T.() -> Unit, + ): Job = + events.buffer(Channel.UNLIMITED) + .filterIsInstance() + .onEach { + runCatching { + if (launch) kordRef.launch { consumer(it) } else consumer(it) + }.onFailure { logger.catching(it) } + }.catch { logger.catching(it) } + .launchIn(scope) + + /** + * @suppress + */ + public suspend inline fun send(event: Event) { + eventPublisher.emit(event) + } + + /** + * Install an [Extension] to this bot. + * + * This function will call the given builder function and store the resulting extension object, ready to be + * set up when the next [ReadyEvent] happens. + * + * @param builder Builder function (or extension constructor) that takes an [ExtensibleBot] instance and + * returns an [Extension]. + */ + @Throws(InvalidExtensionException::class) + public open suspend fun addExtension(builder: () -> Extension) { + val extensionObj = builder.invoke() + + if (extensions.contains(extensionObj.name)) { + logger.error { + "Extension with duplicate name ${extensionObj.name} loaded - unloading previously registered extension" + } + + unloadExtension(extensionObj.name) + } + + extensions[extensionObj.name] = extensionObj + loadExtension(extensionObj.name) + + if (!extensionObj.loaded) { + logger.warn { "Failed to set up extension: ${extensionObj.name}" } + } else { + logger.debug { "Loaded extension: ${extensionObj.name}" } + + settings.hooksBuilder.runExtensionAdded(this, extensionObj) + } + } + + /** + * Reload an unloaded [Extension] from this bot, by name. + * + * This function **does not** create a new extension object - it simply + * calls its `setup()` function. Loaded extensions can + * be unload again by calling [unloadExtension]. + * + * This function simply returns if the extension isn't found. + * + * @param extension The name of the [Extension] to unload. + */ + @Throws(InvalidExtensionException::class) + public open suspend fun loadExtension(extension: String) { + val extensionObj = extensions[extension] ?: return + + if (!extensionObj.loaded) { + extensionObj.doSetup() + } + } + + /** + * Find the first loaded extension that is an instance of the type provided in `T`. + * + * This can be used to find an extension based on, for example, an implemented interface. + * + * @param T Types to match extensions against. + */ + public inline fun findExtension(): T? = + findExtensions().firstOrNull() + + /** + * Find all loaded extensions that are instances of the type provided in `T`. + * + * This can be used to find extensions based on, for example, an implemented interface. + * + * @param T Types to match extensions against. + */ + public inline fun findExtensions(): List = + extensions.values.filterIsInstance() + + /** + * Unload an installed [Extension] from this bot, by name. + * + * This function **does not** remove the extension object - it simply + * removes its event handlers and commands. Unloaded extensions can + * be loaded again by calling [loadExtension]. + * + * This function simply returns if the extension isn't found. + * + * @param extension The name of the [Extension] to unload. + */ + public open suspend fun unloadExtension(extension: String) { + val extensionObj = extensions[extension] ?: return + + if (extensionObj.loaded) { + extensionObj.doUnload() + } + } + + /** + * Remove an installed [Extension] from this bot, by name. + * + * This function will unload the given extension (if it's loaded), and remove the + * extension object from the list of registered extensions. + * + * @param extension The name of the [Extension] to unload. + * + * @suppress This is meant to be used with the module system, and isn't necessarily a user-facing API. + * You need to be quite careful with this! + */ + public open suspend fun removeExtension(extension: String) { + unloadExtension(extension) + + extensions.remove(extension) + } + + /** + * Directly register an [EventHandler] to this bot. + * + * Generally speaking, you shouldn't call this directly - instead, create an [Extension] and + * call the [Extension.event] function in your [Extension.setup] function. + * + * This function will throw an [EventHandlerRegistrationException] if the event handler has already been registered. + * + * @param handler The event handler to be registered. + * @throws EventHandlerRegistrationException Thrown if the event handler could not be registered. + */ + @Throws(EventHandlerRegistrationException::class) + public inline fun addEventHandler(handler: EventHandler) { + if (eventHandlers.contains(handler)) { + throw EventHandlerRegistrationException( + "Event handler already registered in '${handler.extension.name}' extension." + ) + } + + if (initialized) { + handler.listenerRegistrationCallable?.invoke() ?: error( + "Event handler $handler does not have a listener registration callback. This should never happen!" + ) + } + + eventHandlers.add(handler) + } + + /** + * Directly register an [EventHandler] to this bot. + * + * Generally speaking, you shouldn't call this directly - instead, create an [Extension] and + * call the [Extension.event] function in your [Extension.setup] function. + * + * This function will throw an [EventHandlerRegistrationException] if the event handler has already been registered. + * + * @param handler The event handler to be registered. + * @throws EventHandlerRegistrationException Thrown if the event handler could not be registered. + */ + @Throws(EventHandlerRegistrationException::class) + public inline fun registerListenerForHandler(handler: EventHandler): Job { + return on { + handler.call(this) + } + } + + /** + * Directly remove a registered [EventHandler] from this bot. + * + * This function is used when extensions are unloaded, in order to clear out their event handlers. + * No exception is thrown if the event handler wasn't registered. + * + * @param handler The event handler to be removed. + */ + public open fun removeEventHandler(handler: EventHandler): Boolean = eventHandlers.remove(handler) } /** @@ -506,9 +506,9 @@ public open class ExtensibleBot( */ @Suppress("FunctionNaming") // This is a factory function public suspend fun ExtensibleBot(token: String, builder: suspend ExtensibleBotBuilder.() -> Unit): ExtensibleBot { - val settings = ExtensibleBotBuilder() + val settings = ExtensibleBotBuilder() - builder(settings) + builder(settings) - return settings.build(token) + return settings.build(token) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/annotations/DoNotChain.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/annotations/DoNotChain.kt index e2002c4483..da9ff3cbc6 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/annotations/DoNotChain.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/annotations/DoNotChain.kt @@ -8,10 +8,10 @@ package com.kotlindiscord.kord.extensions.annotations /** Marks a class or function that's part of the bot builder DSLs. **/ @RequiresOptIn( - message = "This function will cause an immediate REST call. If you want to do more than one operation here, " + - "you should use `.edit { }` instead as that will result in a single REST call for all operations - instead " + - "of a separate REST call for each operation.", - level = RequiresOptIn.Level.WARNING + message = "This function will cause an immediate REST call. If you want to do more than one operation here, " + + "you should use `.edit { }` instead as that will result in a single REST call for all operations - instead " + + "of a separate REST call for each operation.", + level = RequiresOptIn.Level.WARNING ) @Target(AnnotationTarget.FUNCTION) public annotation class DoNotChain diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Argument.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Argument.kt index 8eb7135929..8842ebcff2 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Argument.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Argument.kt @@ -17,14 +17,14 @@ import com.kotlindiscord.kord.extensions.i18n.TranslationsProvider * @param converter Argument converter to use for this argument. */ public data class Argument( - val displayName: String, - val description: String, - val converter: Converter, + val displayName: String, + val description: String, + val converter: Converter, ) { - init { - converter.argumentObj = this - } + init { + converter.argumentObj = this + } } internal fun Argument<*>.getDefaultTranslatedDisplayName(provider: TranslationsProvider, command: Command): String = - provider.translate(displayName, provider.defaultLocale, command.resolvedBundle ?: converter.bundle) + provider.translate(displayName, provider.defaultLocale, command.resolvedBundle ?: converter.bundle) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Arguments.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Arguments.kt index 1b7c3c1b76..1b10634ebb 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Arguments.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Arguments.kt @@ -16,180 +16,180 @@ import com.kotlindiscord.kord.extensions.commands.converters.* * be wherever you like - as long as they're public. */ public open class Arguments { - /** List of [Argument] objects, which wrap converters. **/ - public val args: MutableList> = mutableListOf() - - /** - * During an autocomplete interaction, whether to try to fill the defined arguments from that event before calling - * the registered callback. - * - * This is only required when you're using a converter that references a previous argument in its autocomplete - * callback, or you've provided a custom autocomplete callback that does the same thing. - * - * When enabled, this will only fill in previous arguments up to the current one. - * Don't enable this if you don't need it, as it may significantly slow down your bot's autocomplete processing. - */ - public open val parseForAutocomplete: Boolean = false - - /** - * Add a [SingleConverter] argument to this set of arguments. - * - * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which - * is intended to be used as a property delegate. - * - * @param displayName Display name used in help messages and as the key for keyword arguments. - * @param converter Converter instance to add. - * - * @return Argument converter to use as a delegate. - */ - public fun arg( - displayName: String, - description: String, - converter: SingleConverter - ): SingleConverter { - args.add(Argument(displayName, description, converter)) - - return converter - } - - /** - * Add a [DefaultingConverter] argument to this set of arguments. - * - * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which - * is intended to be used as a property delegate. - * - * @param displayName Display name used in help messages and as the key for keyword arguments. - * @param converter Converter instance to add. - * - * @return Argument converter to use as a delegate. - */ - public fun arg( - displayName: String, - description: String, - converter: DefaultingConverter - ): DefaultingConverter { - args.add(Argument(displayName, description, converter)) - - return converter - } - - /** - * Add an [OptionalConverter] argument to this set of arguments. - * - * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which - * is intended to be used as a property delegate. - * - * @param displayName Display name used in help messages and as the key for keyword arguments. - * @param converter Converter instance to add. - * - * @return Argument converter to use as a delegate. - */ - public fun arg( - displayName: String, - description: String, - converter: OptionalConverter - ): OptionalConverter { - args.add(Argument(displayName, description, converter)) - - return converter - } - - /** - * Add a [ListConverter] argument to this set of arguments. - * - * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which - * is intended to be used as a property delegate. - * - * @param displayName Display name used in help messages and as the key for keyword arguments. - * @param converter Converter instance to add. - * - * @return Argument converter to use as a delegate. - */ - public fun arg( - displayName: String, - description: String, - converter: ListConverter - ): ListConverter { - args.add(Argument(displayName, description, converter)) - - return converter - } - - /** - * Add a [CoalescingConverter] argument to this set of arguments. - * - * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which - * is intended to be used as a property delegate. - * - * @param displayName Display name used in help messages and as the key for keyword arguments. - * @param converter Converter instance to add. - * - * @return Argument converter to use as a delegate. - */ - public fun arg( - displayName: String, - description: String, - converter: CoalescingConverter - ): CoalescingConverter { - args.add(Argument(displayName, description, converter)) - - return converter - } - - /** - * Add a [DefaultingCoalescingConverter] argument to this set of arguments. - * - * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which - * is intended to be used as a property delegate. - * - * @param displayName Display name used in help messages and as the key for keyword arguments. - * @param converter Converter instance to add. - * - * @return Argument converter to use as a delegate. - */ - public fun arg( - displayName: String, - description: String, - converter: DefaultingCoalescingConverter - ): DefaultingCoalescingConverter { - args.add(Argument(displayName, description, converter)) - - return converter - } - - /** - * Add an [OptionalCoalescingConverter] argument to this set of arguments. - * - * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which - * is intended to be used as a property delegate. - * - * @param displayName Display name used in help messages and as the key for keyword arguments. - * @param converter Converter instance to add. - * - * @return Argument converter to use as a delegate. - */ - public fun arg( - displayName: String, - description: String, - converter: OptionalCoalescingConverter - ): OptionalCoalescingConverter { - args.add(Argument(displayName, description, converter)) - - return converter - } - - /** Validation function that will throw an error if there's a problem with this Arguments class/subclass. **/ - public open fun validate() { - val names: MutableSet = mutableSetOf() - - args.forEach { - val name = it.displayName.lowercase() - - if (name in names) { - error("Duplicate argument name: ${it.displayName}") - } - - names.add(name) - } - } + /** List of [Argument] objects, which wrap converters. **/ + public val args: MutableList> = mutableListOf() + + /** + * During an autocomplete interaction, whether to try to fill the defined arguments from that event before calling + * the registered callback. + * + * This is only required when you're using a converter that references a previous argument in its autocomplete + * callback, or you've provided a custom autocomplete callback that does the same thing. + * + * When enabled, this will only fill in previous arguments up to the current one. + * Don't enable this if you don't need it, as it may significantly slow down your bot's autocomplete processing. + */ + public open val parseForAutocomplete: Boolean = false + + /** + * Add a [SingleConverter] argument to this set of arguments. + * + * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which + * is intended to be used as a property delegate. + * + * @param displayName Display name used in help messages and as the key for keyword arguments. + * @param converter Converter instance to add. + * + * @return Argument converter to use as a delegate. + */ + public fun arg( + displayName: String, + description: String, + converter: SingleConverter, + ): SingleConverter { + args.add(Argument(displayName, description, converter)) + + return converter + } + + /** + * Add a [DefaultingConverter] argument to this set of arguments. + * + * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which + * is intended to be used as a property delegate. + * + * @param displayName Display name used in help messages and as the key for keyword arguments. + * @param converter Converter instance to add. + * + * @return Argument converter to use as a delegate. + */ + public fun arg( + displayName: String, + description: String, + converter: DefaultingConverter, + ): DefaultingConverter { + args.add(Argument(displayName, description, converter)) + + return converter + } + + /** + * Add an [OptionalConverter] argument to this set of arguments. + * + * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which + * is intended to be used as a property delegate. + * + * @param displayName Display name used in help messages and as the key for keyword arguments. + * @param converter Converter instance to add. + * + * @return Argument converter to use as a delegate. + */ + public fun arg( + displayName: String, + description: String, + converter: OptionalConverter, + ): OptionalConverter { + args.add(Argument(displayName, description, converter)) + + return converter + } + + /** + * Add a [ListConverter] argument to this set of arguments. + * + * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which + * is intended to be used as a property delegate. + * + * @param displayName Display name used in help messages and as the key for keyword arguments. + * @param converter Converter instance to add. + * + * @return Argument converter to use as a delegate. + */ + public fun arg( + displayName: String, + description: String, + converter: ListConverter, + ): ListConverter { + args.add(Argument(displayName, description, converter)) + + return converter + } + + /** + * Add a [CoalescingConverter] argument to this set of arguments. + * + * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which + * is intended to be used as a property delegate. + * + * @param displayName Display name used in help messages and as the key for keyword arguments. + * @param converter Converter instance to add. + * + * @return Argument converter to use as a delegate. + */ + public fun arg( + displayName: String, + description: String, + converter: CoalescingConverter, + ): CoalescingConverter { + args.add(Argument(displayName, description, converter)) + + return converter + } + + /** + * Add a [DefaultingCoalescingConverter] argument to this set of arguments. + * + * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which + * is intended to be used as a property delegate. + * + * @param displayName Display name used in help messages and as the key for keyword arguments. + * @param converter Converter instance to add. + * + * @return Argument converter to use as a delegate. + */ + public fun arg( + displayName: String, + description: String, + converter: DefaultingCoalescingConverter, + ): DefaultingCoalescingConverter { + args.add(Argument(displayName, description, converter)) + + return converter + } + + /** + * Add an [OptionalCoalescingConverter] argument to this set of arguments. + * + * This is typically used indirectly, via an extension function that wraps it. It returns the converter, which + * is intended to be used as a property delegate. + * + * @param displayName Display name used in help messages and as the key for keyword arguments. + * @param converter Converter instance to add. + * + * @return Argument converter to use as a delegate. + */ + public fun arg( + displayName: String, + description: String, + converter: OptionalCoalescingConverter, + ): OptionalCoalescingConverter { + args.add(Argument(displayName, description, converter)) + + return converter + } + + /** Validation function that will throw an error if there's a problem with this Arguments class/subclass. **/ + public open fun validate() { + val names: MutableSet = mutableSetOf() + + args.forEach { + val name = it.displayName.lowercase() + + if (name in names) { + error("Duplicate argument name: ${it.displayName}") + } + + names.add(name) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Command.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Command.kt index 29e519fecd..e1e7c7f403 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Command.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Command.kt @@ -39,97 +39,97 @@ import java.util.* */ @ExtensionDSL public abstract class Command(public val extension: Extension) : Lockable, KordExKoinComponent { - /** - * The name of this command, for invocation and help commands. - */ - public open lateinit var name: String - - /** Set this to `true` to lock command execution with a Mutex. **/ - public override var locking: Boolean = false - - /** Translation bundle to use, if not the one provided by the extension. **/ - public var bundle: String? = null - - /** - * @suppress Bundle getter that exists because the extension bundle may have changed by the time the command is - * registered. - */ - public val resolvedBundle: String? - get() = bundle ?: extension.bundle - - override var mutex: Mutex? = null - - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by inject() - - /** Bot settings object. **/ - public val settings: ExtensibleBotBuilder by inject() - - /** Sentry adapter, for easy access to Sentry functions. **/ - public val sentry: SentryAdapter by inject() - - /** Kord instance, backing the ExtensibleBot. **/ - public val kord: Kord by inject() - - /** Permissions required to be able to run this command. **/ - public open val requiredPerms: MutableSet = mutableSetOf() - - /** Translation cache, so we don't have to look up translations every time. **/ - public open val nameTranslationCache: MutableMap = mutableMapOf() - - /** - * An internal function used to ensure that all of a command's required arguments are present and correct. - * - * @throws InvalidCommandException Thrown when a required argument hasn't been set or is invalid. - */ - @Throws(InvalidCommandException::class) - public open fun validate() { - if (!::name.isInitialized || name.isEmpty()) { - throw InvalidCommandException(null, "No command name given.") - } - - if (locking && mutex == null) { - mutex = Mutex() - } - } - - /** Quick shortcut for emitting a command event without blocking. **/ - public open suspend fun emitEventAsync(event: CommandEvent<*, *>): Job = - kord.launch { - extension.bot.send(event) - } - - /** Checks whether the bot has the specified required permissions, throwing if it doesn't. **/ - @Throws(DiscordRelayedException::class) - public open suspend fun checkBotPerms(context: CommandContext) { - if (requiredPerms.isEmpty()) { - return // Nothing to check, don't try to hit the cache - } - - if (context.getGuild() != null) { - val perms = context - .getChannel() - .asChannelOf() - .permissionsForMember(kord.selfId) - - val missingPerms = requiredPerms.filter { !perms.contains(it) } - - if (missingPerms.isNotEmpty()) { - throw DiscordRelayedException( - context.translate( - "commands.error.missingBotPermissions", - null, - - replacements = arrayOf( - missingPerms - .map { it.translate(context.getLocale()) } - .joinToString() - ) - ) - ) - } - } - } + /** + * The name of this command, for invocation and help commands. + */ + public open lateinit var name: String + + /** Set this to `true` to lock command execution with a Mutex. **/ + public override var locking: Boolean = false + + /** Translation bundle to use, if not the one provided by the extension. **/ + public var bundle: String? = null + + /** + * @suppress Bundle getter that exists because the extension bundle may have changed by the time the command is + * registered. + */ + public val resolvedBundle: String? + get() = bundle ?: extension.bundle + + override var mutex: Mutex? = null + + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by inject() + + /** Bot settings object. **/ + public val settings: ExtensibleBotBuilder by inject() + + /** Sentry adapter, for easy access to Sentry functions. **/ + public val sentry: SentryAdapter by inject() + + /** Kord instance, backing the ExtensibleBot. **/ + public val kord: Kord by inject() + + /** Permissions required to be able to run this command. **/ + public open val requiredPerms: MutableSet = mutableSetOf() + + /** Translation cache, so we don't have to look up translations every time. **/ + public open val nameTranslationCache: MutableMap = mutableMapOf() + + /** + * An internal function used to ensure that all of a command's required arguments are present and correct. + * + * @throws InvalidCommandException Thrown when a required argument hasn't been set or is invalid. + */ + @Throws(InvalidCommandException::class) + public open fun validate() { + if (!::name.isInitialized || name.isEmpty()) { + throw InvalidCommandException(null, "No command name given.") + } + + if (locking && mutex == null) { + mutex = Mutex() + } + } + + /** Quick shortcut for emitting a command event without blocking. **/ + public open suspend fun emitEventAsync(event: CommandEvent<*, *>): Job = + kord.launch { + extension.bot.send(event) + } + + /** Checks whether the bot has the specified required permissions, throwing if it doesn't. **/ + @Throws(DiscordRelayedException::class) + public open suspend fun checkBotPerms(context: CommandContext) { + if (requiredPerms.isEmpty()) { + return // Nothing to check, don't try to hit the cache + } + + if (context.getGuild() != null) { + val perms = context + .getChannel() + .asChannelOf() + .permissionsForMember(kord.selfId) + + val missingPerms = requiredPerms.filter { !perms.contains(it) } + + if (missingPerms.isNotEmpty()) { + throw DiscordRelayedException( + context.translate( + "commands.error.missingBotPermissions", + null, + + replacements = arrayOf( + missingPerms + .map { it.translate(context.getLocale()) } + .joinToString() + ) + ) + ) + } + } + } /** If your bot requires permissions to be able to execute the command, add them using this function. **/ public fun requireBotPermissions(vararg perms: Permission) { diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/CommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/CommandContext.kt index c3706a3cfb..beac1d6e70 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/CommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/CommandContext.kt @@ -36,79 +36,79 @@ import java.util.* */ @ExtensionDSL public abstract class CommandContext( - public open val command: Command, - public open val eventObj: Event, - public open val commandName: String, - public open val cache: MutableStringKeyedMap, + public open val command: Command, + public open val eventObj: Event, + public open val commandName: String, + public open val cache: MutableStringKeyedMap, ) : KordExKoinComponent, TranslatableContext { - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by lazy { getTranslationProvider() } + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by lazy { getTranslationProvider() } - /** Current Sentry context, containing breadcrumbs and other goodies. **/ - public val sentry: SentryContext = SentryContext() + /** Current Sentry context, containing breadcrumbs and other goodies. **/ + public val sentry: SentryContext = SentryContext() - public override var resolvedLocale: Locale? = null + public override var resolvedLocale: Locale? = null - override val bundle: String? - get() = command.resolvedBundle + override val bundle: String? + get() = command.resolvedBundle - /** Called before processing, used to populate any extra variables from event data. **/ - public abstract suspend fun populate() + /** Called before processing, used to populate any extra variables from event data. **/ + public abstract suspend fun populate() - /** Extract channel information from event data. **/ - public abstract suspend fun getChannel(): ChannelBehavior + /** Extract channel information from event data. **/ + public abstract suspend fun getChannel(): ChannelBehavior - /** Extract guild information from event data, if that context is available. **/ - public abstract suspend fun getGuild(): GuildBehavior? + /** Extract guild information from event data, if that context is available. **/ + public abstract suspend fun getGuild(): GuildBehavior? - /** Extract member information from event data, if that context is available. **/ - public abstract suspend fun getMember(): MemberBehavior? + /** Extract member information from event data, if that context is available. **/ + public abstract suspend fun getMember(): MemberBehavior? - /** Extract user information from event data, if that context is available. **/ - public abstract suspend fun getUser(): UserBehavior? + /** Extract user information from event data, if that context is available. **/ + public abstract suspend fun getUser(): UserBehavior? - public override suspend fun getLocale(): Locale { - var locale: Locale? = resolvedLocale + public override suspend fun getLocale(): Locale { + var locale: Locale? = resolvedLocale - if (locale != null) { - return locale - } + if (locale != null) { + return locale + } - val guild = guildFor(eventObj) - val channel = channelFor(eventObj) - val user = userFor(eventObj) + val guild = guildFor(eventObj) + val channel = channelFor(eventObj) + val user = userFor(eventObj) - for (resolver in command.extension.bot.settings.i18nBuilder.localeResolvers) { - val result = resolver(guild, channel, user, interactionFor(eventObj)) + for (resolver in command.extension.bot.settings.i18nBuilder.localeResolvers) { + val result = resolver(guild, channel, user, interactionFor(eventObj)) - if (result != null) { - locale = result - break - } - } + if (result != null) { + locale = result + break + } + } - resolvedLocale = locale ?: command.extension.bot.settings.i18nBuilder.defaultLocale + resolvedLocale = locale ?: command.extension.bot.settings.i18nBuilder.defaultLocale - return resolvedLocale!! - } + return resolvedLocale!! + } - public override suspend fun translate( - key: String, - bundleName: String?, - replacements: Array, - ): String { - val locale = getLocale() + public override suspend fun translate( + key: String, + bundleName: String?, + replacements: Array, + ): String { + val locale = getLocale() - return translationsProvider.translate(key, locale, bundleName, replacements) - } + return translationsProvider.translate(key, locale, bundleName, replacements) + } - public override suspend fun translate( - key: String, - bundleName: String?, - replacements: Map, - ): String { - val locale = getLocale() + public override suspend fun translate( + key: String, + bundleName: String?, + replacements: Map, + ): String { + val locale = getLocale() - return translationsProvider.translate(key, locale, bundleName, replacements) - } + return translationsProvider.translate(key, locale, bundleName, replacements) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommand.kt index 36df9e4f74..b0009625b9 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommand.kt @@ -32,175 +32,175 @@ import dev.kord.common.Locale as KLocale * @param extension Extension this application command belongs to. */ public abstract class ApplicationCommand( - extension: Extension + extension: Extension, ) : Command(extension), KordExKoinComponent { - /** Translations provider, for retrieving translations. **/ - protected val bot: ExtensibleBot by inject() - - /** Quick access to the command registry. **/ - public val registry: ApplicationCommandRegistry by inject() - - /** Discord-side command type, for matching up. **/ - public abstract val type: ApplicationCommandType - - /** @suppress **/ - public open val checkList: MutableList> = mutableListOf() - - /** @suppress **/ - public open var guildId: Snowflake? = settings.applicationCommandsBuilder.defaultGuild - - /** - * Whether to allow everyone to use this command by default. - * - * This will be set to `false` automatically by the `allowX` functions, to ensure that they're applied by Discord. - */ - public open var allowByDefault: Boolean - get() = defaultMemberPermissions == null - set(value) { - defaultMemberPermissions = if (value) { - null - } else { - Permissions() - } - } - - /** - * Default set of [Permissions] required to use the command on a guild. - * - * **Not enforced, read [requirePermission] for more information** - */ - public open var defaultMemberPermissions: Permissions? = null - - /** - * Enables or disables the command in DMs. - * - * **Calling [guild] or setting [guildId] will disable this automatically** - */ - public open var allowInDms: Boolean = extension.allowApplicationCommandInDMs - get() { - if (guildId != null) { - return false - } - - return field - } - - /** Permissions required to be able to run this command. **/ - public override val requiredPerms: MutableSet = mutableSetOf() - - /** - * A [Localized] version of [name]. Lower-cased if this is a slash command. - */ - public val localizedName: Localized by lazy { localize(name, this is SlashCommand<*, *, *>) } - - /** - * This will register a requirement for [permissions] with Discord. - * - * **These permissions won't get enforced, as Discords UI allows server owners to change them, if you want to - * enforce them please also call [hasPermission]** - */ - public fun requirePermission(vararg permissions: Permission) { - val newPermissions = (defaultMemberPermissions ?: Permissions()) + Permissions(*permissions) - defaultMemberPermissions = newPermissions - } - - /** - * Localizes a property by its [key] for this command. - * - * @param lowerCase Provide `true` to lower-case all the translations. Discord requires this for some fields. - */ - public fun localize(key: String, lowerCase: Boolean = false): Localized { - var default = translationsProvider.translate( - key, - this.resolvedBundle, - translationsProvider.defaultLocale - ) - - if (lowerCase) { - default = default.lowercase(translationsProvider.defaultLocale) - } - - val translations = bot.settings.i18nBuilder.applicationCommandLocales - .associateWith { locale -> - val result = translationsProvider.translate( - key, - this.resolvedBundle, - locale.asJavaLocale() - ) - - if (lowerCase) { - result.lowercase(locale.asJavaLocale()) - } else { - result - } - }.filter { it.value != default } - - return Localized(default, translations.toMutableMap()) - } - - /** Specify a specific guild for this application command to be locked to. **/ - public open fun guild(guild: Snowflake?) { - this.guildId = guild - } - - /** - * Define a check which must pass for the command to be executed. - * - * A command may have multiple checks - all checks must pass for the command to be executed. - * Checks will be run in the order that they're defined. - * - * This function can be used DSL-style with a given body, or it can be passed one or more - * predefined functions. See the samples for more information. - * - * @param checks Checks to apply to this command. - */ - public open fun check(vararg checks: CheckWithCache) { - checkList.addAll(checks) - } - - /** - * Overloaded check function to allow for DSL syntax. - * - * @param check Check to apply to this command. - */ - public open fun check(check: CheckWithCache) { - checkList.add(check) - } - - /** Called in order to execute the command. **/ - public open suspend fun doCall(event: E): Unit = withLock { - val cache: MutableStringKeyedMap = mutableMapOf() - - call(event, cache) - } - - /** Runs standard checks that can be handled in a generic way, without worrying about subclass-specific checks. **/ - @Throws(DiscordRelayedException::class) - public open suspend fun runStandardChecks(event: E, cache: MutableStringKeyedMap): Boolean { - val locale = event.getLocale() - - checkList.forEach { check -> - val context = CheckContextWithCache(event, locale, cache) - - check(context) - - if (!context.passed) { - context.throwIfFailedWithMessage() - - return false - } - } - - return true - } - - /** Override this in order to implement any subclass-specific checks. **/ - @Throws(DiscordRelayedException::class) - public open suspend fun runChecks(event: E, cache: MutableStringKeyedMap): Boolean = - runStandardChecks(event, cache) - - /** Override this to implement the calling logic for your subclass. **/ - public abstract suspend fun call(event: E, cache: MutableStringKeyedMap) + /** Translations provider, for retrieving translations. **/ + protected val bot: ExtensibleBot by inject() + + /** Quick access to the command registry. **/ + public val registry: ApplicationCommandRegistry by inject() + + /** Discord-side command type, for matching up. **/ + public abstract val type: ApplicationCommandType + + /** @suppress **/ + public open val checkList: MutableList> = mutableListOf() + + /** @suppress **/ + public open var guildId: Snowflake? = settings.applicationCommandsBuilder.defaultGuild + + /** + * Whether to allow everyone to use this command by default. + * + * This will be set to `false` automatically by the `allowX` functions, to ensure that they're applied by Discord. + */ + public open var allowByDefault: Boolean + get() = defaultMemberPermissions == null + set(value) { + defaultMemberPermissions = if (value) { + null + } else { + Permissions() + } + } + + /** + * Default set of [Permissions] required to use the command on a guild. + * + * **Not enforced, read [requirePermission] for more information** + */ + public open var defaultMemberPermissions: Permissions? = null + + /** + * Enables or disables the command in DMs. + * + * **Calling [guild] or setting [guildId] will disable this automatically** + */ + public open var allowInDms: Boolean = extension.allowApplicationCommandInDMs + get() { + if (guildId != null) { + return false + } + + return field + } + + /** Permissions required to be able to run this command. **/ + public override val requiredPerms: MutableSet = mutableSetOf() + + /** + * A [Localized] version of [name]. Lower-cased if this is a slash command. + */ + public val localizedName: Localized by lazy { localize(name, this is SlashCommand<*, *, *>) } + + /** + * This will register a requirement for [permissions] with Discord. + * + * **These permissions won't get enforced, as Discords UI allows server owners to change them, if you want to + * enforce them please also call [hasPermission]** + */ + public fun requirePermission(vararg permissions: Permission) { + val newPermissions = (defaultMemberPermissions ?: Permissions()) + Permissions(*permissions) + defaultMemberPermissions = newPermissions + } + + /** + * Localizes a property by its [key] for this command. + * + * @param lowerCase Provide `true` to lower-case all the translations. Discord requires this for some fields. + */ + public fun localize(key: String, lowerCase: Boolean = false): Localized { + var default = translationsProvider.translate( + key, + this.resolvedBundle, + translationsProvider.defaultLocale + ) + + if (lowerCase) { + default = default.lowercase(translationsProvider.defaultLocale) + } + + val translations = bot.settings.i18nBuilder.applicationCommandLocales + .associateWith { locale -> + val result = translationsProvider.translate( + key, + this.resolvedBundle, + locale.asJavaLocale() + ) + + if (lowerCase) { + result.lowercase(locale.asJavaLocale()) + } else { + result + } + }.filter { it.value != default } + + return Localized(default, translations.toMutableMap()) + } + + /** Specify a specific guild for this application command to be locked to. **/ + public open fun guild(guild: Snowflake?) { + this.guildId = guild + } + + /** + * Define a check which must pass for the command to be executed. + * + * A command may have multiple checks - all checks must pass for the command to be executed. + * Checks will be run in the order that they're defined. + * + * This function can be used DSL-style with a given body, or it can be passed one or more + * predefined functions. See the samples for more information. + * + * @param checks Checks to apply to this command. + */ + public open fun check(vararg checks: CheckWithCache) { + checkList.addAll(checks) + } + + /** + * Overloaded check function to allow for DSL syntax. + * + * @param check Check to apply to this command. + */ + public open fun check(check: CheckWithCache) { + checkList.add(check) + } + + /** Called in order to execute the command. **/ + public open suspend fun doCall(event: E): Unit = withLock { + val cache: MutableStringKeyedMap = mutableMapOf() + + call(event, cache) + } + + /** Runs standard checks that can be handled in a generic way, without worrying about subclass-specific checks. **/ + @Throws(DiscordRelayedException::class) + public open suspend fun runStandardChecks(event: E, cache: MutableStringKeyedMap): Boolean { + val locale = event.getLocale() + + checkList.forEach { check -> + val context = CheckContextWithCache(event, locale, cache) + + check(context) + + if (!context.passed) { + context.throwIfFailedWithMessage() + + return false + } + } + + return true + } + + /** Override this in order to implement any subclass-specific checks. **/ + @Throws(DiscordRelayedException::class) + public open suspend fun runChecks(event: E, cache: MutableStringKeyedMap): Boolean = + runStandardChecks(event, cache) + + /** Override this to implement the calling logic for your subclass. **/ + public abstract suspend fun call(event: E, cache: MutableStringKeyedMap) } /** diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandContext.kt index f3c120bb09..96d7a53598 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandContext.kt @@ -29,55 +29,55 @@ import org.koin.core.component.inject * @param genericCommand Generic command object that this context belongs to. */ public abstract class ApplicationCommandContext( - public val genericEvent: ApplicationCommandInteractionCreateEvent, - public val genericCommand: ApplicationCommand<*>, - cache: MutableStringKeyedMap + public val genericEvent: ApplicationCommandInteractionCreateEvent, + public val genericCommand: ApplicationCommand<*>, + cache: MutableStringKeyedMap, ) : CommandContext(genericCommand, genericEvent, genericCommand.name, cache) { - /** Current bot setting object. **/ - public val botSettings: ExtensibleBotBuilder by inject() + /** Current bot setting object. **/ + public val botSettings: ExtensibleBotBuilder by inject() - /** Channel this command was executed within. **/ - public open lateinit var channel: MessageChannelBehavior + /** Channel this command was executed within. **/ + public open lateinit var channel: MessageChannelBehavior - /** Guild this command was executed within, if any. **/ - public open var guild: GuildBehavior? = null + /** Guild this command was executed within, if any. **/ + public open var guild: GuildBehavior? = null - /** Member that executed this command, if on a guild. **/ - public open var member: MemberBehavior? = null + /** Member that executed this command, if on a guild. **/ + public open var member: MemberBehavior? = null - /** User that executed this command. **/ - public open lateinit var user: UserBehavior + /** User that executed this command. **/ + public open lateinit var user: UserBehavior - /** - * The permissions applicable to your bot in this execution context (guild, roles, channels), or null if - * this command wasn't executed on a guild. - */ - public val appPermissions: Permissions? = (genericEvent.interaction as? GuildApplicationCommandInteraction) - ?.appPermissions + /** + * The permissions applicable to your bot in this execution context (guild, roles, channels), or null if + * this command wasn't executed on a guild. + */ + public val appPermissions: Permissions? = (genericEvent.interaction as? GuildApplicationCommandInteraction) + ?.appPermissions - /** Called before processing, used to populate any extra variables from event data. **/ - public override suspend fun populate() { - // NOTE: This must always be alphabetical, some latter calls rely on earlier ones + /** Called before processing, used to populate any extra variables from event data. **/ + public override suspend fun populate() { + // NOTE: This must always be alphabetical, some latter calls rely on earlier ones - channel = getChannel() - guild = getGuild() - member = getMember() - user = getUser() - } + channel = getChannel() + guild = getGuild() + member = getMember() + user = getUser() + } - /** Extract channel information from event data, if that context is available. **/ - public override suspend fun getChannel(): MessageChannelBehavior = - genericEvent.interaction.channel + /** Extract channel information from event data, if that context is available. **/ + public override suspend fun getChannel(): MessageChannelBehavior = + genericEvent.interaction.channel - /** Extract guild information from event data, if that context is available. **/ - public override suspend fun getGuild(): GuildBehavior? = - genericEvent.interaction.data.guildId.value?.let { genericEvent.kord.unsafe.guild(it) } + /** Extract guild information from event data, if that context is available. **/ + public override suspend fun getGuild(): GuildBehavior? = + genericEvent.interaction.data.guildId.value?.let { genericEvent.kord.unsafe.guild(it) } - /** Extract member information from event data, if that context is available. **/ - public override suspend fun getMember(): MemberBehavior? = - (genericEvent.interaction as? GuildApplicationCommandInteraction)?.user + /** Extract member information from event data, if that context is available. **/ + public override suspend fun getMember(): MemberBehavior? = + (genericEvent.interaction as? GuildApplicationCommandInteraction)?.user - /** Extract user information from event data, if that context is available. **/ - public override suspend fun getUser(): UserBehavior = - genericEvent.interaction.user + /** Extract user information from event data, if that context is available. **/ + public override suspend fun getUser(): UserBehavior = + genericEvent.interaction.user } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandRegistry.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandRegistry.kt index b0a4699fde..8981d649b3 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandRegistry.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandRegistry.kt @@ -5,13 +5,13 @@ */ @file:Suppress( - "UNCHECKED_CAST", - "TooGenericExceptionCaught", - "StringLiteralDuplication", + "UNCHECKED_CAST", + "TooGenericExceptionCaught", + "StringLiteralDuplication", ) @file:OptIn( - KordUnsafe::class, - KordExperimental::class + KordUnsafe::class, + KordExperimental::class ) package com.kotlindiscord.kord.extensions.commands.application @@ -59,533 +59,533 @@ import javax.naming.InvalidNameException */ public abstract class ApplicationCommandRegistry : KordExKoinComponent { - protected val logger: KLogger = KotlinLogging.logger { } + protected val logger: KLogger = KotlinLogging.logger { } - /** Current instance of the bot. **/ - public open val bot: ExtensibleBot by inject() + /** Current instance of the bot. **/ + public open val bot: ExtensibleBot by inject() - /** Kord instance, backing the ExtensibleBot. **/ - public open val kord: Kord by inject() + /** Kord instance, backing the ExtensibleBot. **/ + public open val kord: Kord by inject() - /** Translations provider, for retrieving translations. **/ - public open val translationsProvider: TranslationsProvider by inject() + /** Translations provider, for retrieving translations. **/ + public open val translationsProvider: TranslationsProvider by inject() - /** Command parser to use for slash commands. **/ - public val argumentParser: SlashCommandParser = SlashCommandParser() + /** Command parser to use for slash commands. **/ + public val argumentParser: SlashCommandParser = SlashCommandParser() - /** Whether the initial sync has been finished, and commands should be registered directly. **/ - public var initialised: Boolean = false - - /** Quick access to the human-readable name for a Discord application command type. **/ - public val ApplicationCommandType.name: String - get() = when (this) { - is ApplicationCommandType.Unknown -> "unknown" - - ApplicationCommandType.ChatInput -> "slash" - ApplicationCommandType.Message -> "message" - ApplicationCommandType.User -> "user" - } - - /** Handles the initial registration of commands, after extensions have been loaded. **/ - public suspend fun initialRegistration() { - if (initialised) { - return - } - - val commands: MutableList> = mutableListOf() - - bot.extensions.values.forEach { - commands += it.messageCommands - commands += it.slashCommands - commands += it.userCommands - } - - try { - initialize(commands) - } catch (t: Throwable) { - logger.error(t) { "Failed to initialize registry" } - } - - initialised = true - } - - /** Called once the initial registration started and all extensions are loaded. **/ - protected abstract suspend fun initialize(commands: List>) - - /** Register a [SlashCommand] to the registry. - * - * This method is only called after the [initialize] method and allows runtime modifications. - */ - public abstract suspend fun register(command: SlashCommand<*, *, *>): SlashCommand<*, *, *>? - - /** - * Register a [MessageCommand] to the registry. - * - * This method is only called after the [initialize] method and allows runtime modifications. - */ - public abstract suspend fun register(command: MessageCommand<*, *>): MessageCommand<*, *>? - - /** Register a [UserCommand] to the registry. - * - * This method is only called after the [initialize] method and allows runtime modifications. - */ - public abstract suspend fun register(command: UserCommand<*, *>): UserCommand<*, *>? - - /** Event handler for slash commands. **/ - public abstract suspend fun handle(event: ChatInputCommandInteractionCreateEvent) - - /** Event handler for message commands. **/ - public abstract suspend fun handle(event: MessageCommandInteractionCreateEvent) - - /** Event handler for user commands. **/ - public abstract suspend fun handle(event: UserCommandInteractionCreateEvent) - - /** Event handler for autocomplete interactions. **/ - public abstract suspend fun handle(event: AutoCompleteInteractionCreateEvent) - - /** Unregister a slash command. **/ - public abstract suspend fun unregister( - command: SlashCommand<*, *, *>, - delete: Boolean = true - ): SlashCommand<*, *, *>? - - /** Unregister a message command. **/ - public abstract suspend fun unregister(command: MessageCommand<*, *>, delete: Boolean = true): MessageCommand<*, *>? - - /** Unregister a user command. **/ - public abstract suspend fun unregister(command: UserCommand<*, *>, delete: Boolean = true): UserCommand<*, *>? - - // region: Utilities - - /** Unregister a generic [ApplicationCommand]. **/ - public open suspend fun unregisterGeneric( - command: ApplicationCommand<*>, - delete: Boolean = true, - ): ApplicationCommand<*>? = - when (command) { - is MessageCommand<*, *> -> unregister(command, delete) - is SlashCommand<*, *, *> -> unregister(command, delete) - is UserCommand<*, *> -> unregister(command, delete) - - else -> error("Unsupported application command type: ${command.type.name}") - } - - /** @suppress Internal function used to delete the given command from Discord. Used by [unregister]. **/ - public open suspend fun deleteGeneric( - command: ApplicationCommand<*>, - discordCommandId: Snowflake, - ) { - try { - if (command.guildId != null) { - kord.unsafe.guildApplicationCommand( - command.guildId!!, - kord.resources.applicationId, - discordCommandId - ).delete() - } else { - kord.unsafe.globalApplicationCommand(kord.resources.applicationId, discordCommandId).delete() - } - } catch (e: KtorRequestException) { - logger.warn(e) { - "Failed to delete ${command.type.name} command ${command.name}" + - if (e.error?.message != null) { - "\n Discord error message: ${e.error?.message}" - } else { - "" - } - } - } - } - - /** Register multiple slash commands. **/ - public open suspend fun > registerAll(vararg commands: T): List = - commands.sortedByDescending { it.name }.mapNotNull { - try { - when (it) { - is SlashCommand<*, *, *> -> register(it) as T - is MessageCommand<*, *> -> register(it) as T - is UserCommand<*, *> -> register(it) as T - - else -> throw IllegalArgumentException( - "The registry does not know about this type of ApplicationCommand" - ) - } - } catch (e: KtorRequestException) { - logger.warn(e) { - "Failed to register ${it.type.name} command: ${it.name}" + - if (e.error?.message != null) { - "\n Discord error message: ${e.error?.message}" - } else { - "" - } - } - - null - } catch (t: Throwable) { - logger.warn(t) { "Failed to register ${it.type.name} command: ${it.name}" } - - null - } - } - - /** - * Creates a KordEx [ApplicationCommand] as discord command and returns the created command's id as [Snowflake]. - */ - public open suspend fun createDiscordCommand(command: ApplicationCommand<*>): Snowflake? = when (command) { - is SlashCommand<*, *, *> -> createDiscordSlashCommand(command) - is UserCommand<*, *> -> createDiscordUserCommand(command) - is MessageCommand<*, *> -> createDiscordMessageCommand(command) - - else -> throw IllegalArgumentException("Unknown ApplicationCommand type") - } - - /** - * Creates a KordEx [SlashCommand] as discord command and returns the created command's id as [Snowflake]. - */ - public open suspend fun createDiscordSlashCommand(command: SlashCommand<*, *, *>): Snowflake? { - val locale = bot.settings.i18nBuilder.defaultLocale - - val guild = if (command.guildId != null) { - kord.getGuildOrNull(command.guildId!!) - } else { - null - } - - val (name, nameLocalizations) = command.localizedName - val (description, descriptionLocalizations) = command.localizedDescription - - val response = if (guild == null) { - // We're registering global commands here, if the guild is null - - kord.createGlobalChatInputCommand(name, description) { - logger.trace { "Adding/updating global ${command.type.name} command: $name" } - - this.nameLocalizations = nameLocalizations - this.descriptionLocalizations = descriptionLocalizations - - this.register(locale, command) - } - } else { - // We're registering guild-specific commands here, if the guild is available - - guild.createChatInputCommand(name, description) { - logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" } - - this.nameLocalizations = nameLocalizations - this.descriptionLocalizations = descriptionLocalizations - - this.register(locale, command) - } - } - - return response.id - } - - /** - * Creates a KordEx [UserCommand] as discord command and returns the created command's id as [Snowflake]. - */ - public open suspend fun createDiscordUserCommand(command: UserCommand<*, *>): Snowflake? { - val locale = bot.settings.i18nBuilder.defaultLocale - - val guild = if (command.guildId != null) { - kord.getGuildOrNull(command.guildId!!) - } else { - null - } - - val (name, nameLocalizations) = command.localizedName - - val response = if (guild == null) { - // We're registering global commands here, if the guild is null - - kord.createGlobalUserCommand(name) { - logger.trace { "Adding/updating global ${command.type.name} command: $name" } - this.nameLocalizations = nameLocalizations - - this.register(locale, command) - } - } else { - // We're registering guild-specific commands here, if the guild is available - - guild.createUserCommand(name) { - logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" } - this.nameLocalizations = nameLocalizations + /** Whether the initial sync has been finished, and commands should be registered directly. **/ + public var initialised: Boolean = false + + /** Quick access to the human-readable name for a Discord application command type. **/ + public val ApplicationCommandType.name: String + get() = when (this) { + is ApplicationCommandType.Unknown -> "unknown" + + ApplicationCommandType.ChatInput -> "slash" + ApplicationCommandType.Message -> "message" + ApplicationCommandType.User -> "user" + } + + /** Handles the initial registration of commands, after extensions have been loaded. **/ + public suspend fun initialRegistration() { + if (initialised) { + return + } + + val commands: MutableList> = mutableListOf() + + bot.extensions.values.forEach { + commands += it.messageCommands + commands += it.slashCommands + commands += it.userCommands + } + + try { + initialize(commands) + } catch (t: Throwable) { + logger.error(t) { "Failed to initialize registry" } + } + + initialised = true + } + + /** Called once the initial registration started and all extensions are loaded. **/ + protected abstract suspend fun initialize(commands: List>) + + /** Register a [SlashCommand] to the registry. + * + * This method is only called after the [initialize] method and allows runtime modifications. + */ + public abstract suspend fun register(command: SlashCommand<*, *, *>): SlashCommand<*, *, *>? + + /** + * Register a [MessageCommand] to the registry. + * + * This method is only called after the [initialize] method and allows runtime modifications. + */ + public abstract suspend fun register(command: MessageCommand<*, *>): MessageCommand<*, *>? + + /** Register a [UserCommand] to the registry. + * + * This method is only called after the [initialize] method and allows runtime modifications. + */ + public abstract suspend fun register(command: UserCommand<*, *>): UserCommand<*, *>? + + /** Event handler for slash commands. **/ + public abstract suspend fun handle(event: ChatInputCommandInteractionCreateEvent) + + /** Event handler for message commands. **/ + public abstract suspend fun handle(event: MessageCommandInteractionCreateEvent) + + /** Event handler for user commands. **/ + public abstract suspend fun handle(event: UserCommandInteractionCreateEvent) + + /** Event handler for autocomplete interactions. **/ + public abstract suspend fun handle(event: AutoCompleteInteractionCreateEvent) + + /** Unregister a slash command. **/ + public abstract suspend fun unregister( + command: SlashCommand<*, *, *>, + delete: Boolean = true, + ): SlashCommand<*, *, *>? + + /** Unregister a message command. **/ + public abstract suspend fun unregister(command: MessageCommand<*, *>, delete: Boolean = true): MessageCommand<*, *>? + + /** Unregister a user command. **/ + public abstract suspend fun unregister(command: UserCommand<*, *>, delete: Boolean = true): UserCommand<*, *>? + + // region: Utilities + + /** Unregister a generic [ApplicationCommand]. **/ + public open suspend fun unregisterGeneric( + command: ApplicationCommand<*>, + delete: Boolean = true, + ): ApplicationCommand<*>? = + when (command) { + is MessageCommand<*, *> -> unregister(command, delete) + is SlashCommand<*, *, *> -> unregister(command, delete) + is UserCommand<*, *> -> unregister(command, delete) + + else -> error("Unsupported application command type: ${command.type.name}") + } + + /** @suppress Internal function used to delete the given command from Discord. Used by [unregister]. **/ + public open suspend fun deleteGeneric( + command: ApplicationCommand<*>, + discordCommandId: Snowflake, + ) { + try { + if (command.guildId != null) { + kord.unsafe.guildApplicationCommand( + command.guildId!!, + kord.resources.applicationId, + discordCommandId + ).delete() + } else { + kord.unsafe.globalApplicationCommand(kord.resources.applicationId, discordCommandId).delete() + } + } catch (e: KtorRequestException) { + logger.warn(e) { + "Failed to delete ${command.type.name} command ${command.name}" + + if (e.error?.message != null) { + "\n Discord error message: ${e.error?.message}" + } else { + "" + } + } + } + } + + /** Register multiple slash commands. **/ + public open suspend fun > registerAll(vararg commands: T): List = + commands.sortedByDescending { it.name }.mapNotNull { + try { + when (it) { + is SlashCommand<*, *, *> -> register(it) as T + is MessageCommand<*, *> -> register(it) as T + is UserCommand<*, *> -> register(it) as T + + else -> throw IllegalArgumentException( + "The registry does not know about this type of ApplicationCommand" + ) + } + } catch (e: KtorRequestException) { + logger.warn(e) { + "Failed to register ${it.type.name} command: ${it.name}" + + if (e.error?.message != null) { + "\n Discord error message: ${e.error?.message}" + } else { + "" + } + } + + null + } catch (t: Throwable) { + logger.warn(t) { "Failed to register ${it.type.name} command: ${it.name}" } + + null + } + } + + /** + * Creates a KordEx [ApplicationCommand] as discord command and returns the created command's id as [Snowflake]. + */ + public open suspend fun createDiscordCommand(command: ApplicationCommand<*>): Snowflake? = when (command) { + is SlashCommand<*, *, *> -> createDiscordSlashCommand(command) + is UserCommand<*, *> -> createDiscordUserCommand(command) + is MessageCommand<*, *> -> createDiscordMessageCommand(command) + + else -> throw IllegalArgumentException("Unknown ApplicationCommand type") + } + + /** + * Creates a KordEx [SlashCommand] as discord command and returns the created command's id as [Snowflake]. + */ + public open suspend fun createDiscordSlashCommand(command: SlashCommand<*, *, *>): Snowflake? { + val locale = bot.settings.i18nBuilder.defaultLocale + + val guild = if (command.guildId != null) { + kord.getGuildOrNull(command.guildId!!) + } else { + null + } + + val (name, nameLocalizations) = command.localizedName + val (description, descriptionLocalizations) = command.localizedDescription + + val response = if (guild == null) { + // We're registering global commands here, if the guild is null + + kord.createGlobalChatInputCommand(name, description) { + logger.trace { "Adding/updating global ${command.type.name} command: $name" } + + this.nameLocalizations = nameLocalizations + this.descriptionLocalizations = descriptionLocalizations + + this.register(locale, command) + } + } else { + // We're registering guild-specific commands here, if the guild is available + + guild.createChatInputCommand(name, description) { + logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" } + + this.nameLocalizations = nameLocalizations + this.descriptionLocalizations = descriptionLocalizations + + this.register(locale, command) + } + } + + return response.id + } + + /** + * Creates a KordEx [UserCommand] as discord command and returns the created command's id as [Snowflake]. + */ + public open suspend fun createDiscordUserCommand(command: UserCommand<*, *>): Snowflake? { + val locale = bot.settings.i18nBuilder.defaultLocale + + val guild = if (command.guildId != null) { + kord.getGuildOrNull(command.guildId!!) + } else { + null + } + + val (name, nameLocalizations) = command.localizedName + + val response = if (guild == null) { + // We're registering global commands here, if the guild is null + + kord.createGlobalUserCommand(name) { + logger.trace { "Adding/updating global ${command.type.name} command: $name" } + this.nameLocalizations = nameLocalizations + + this.register(locale, command) + } + } else { + // We're registering guild-specific commands here, if the guild is available + + guild.createUserCommand(name) { + logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" } + this.nameLocalizations = nameLocalizations - this.register(locale, command) - } - } + this.register(locale, command) + } + } - return response.id - } + return response.id + } - /** - * Creates a KordEx [MessageCommand] as discord command and returns the created command's id as [Snowflake]. - */ - public open suspend fun createDiscordMessageCommand(command: MessageCommand<*, *>): Snowflake? { - val locale = bot.settings.i18nBuilder.defaultLocale + /** + * Creates a KordEx [MessageCommand] as discord command and returns the created command's id as [Snowflake]. + */ + public open suspend fun createDiscordMessageCommand(command: MessageCommand<*, *>): Snowflake? { + val locale = bot.settings.i18nBuilder.defaultLocale - val guild = if (command.guildId != null) { - kord.getGuildOrNull(command.guildId!!) - } else { - null - } + val guild = if (command.guildId != null) { + kord.getGuildOrNull(command.guildId!!) + } else { + null + } - val (name, nameLocalizations) = command.localizedName + val (name, nameLocalizations) = command.localizedName - val response = if (guild == null) { - // We're registering global commands here, if the guild is null + val response = if (guild == null) { + // We're registering global commands here, if the guild is null - kord.createGlobalMessageCommand(name) { - logger.trace { "Adding/updating global ${command.type.name} command: $name" } - this.nameLocalizations = nameLocalizations + kord.createGlobalMessageCommand(name) { + logger.trace { "Adding/updating global ${command.type.name} command: $name" } + this.nameLocalizations = nameLocalizations - this.register(locale, command) - } - } else { - // We're registering guild-specific commands here, if the guild is available + this.register(locale, command) + } + } else { + // We're registering guild-specific commands here, if the guild is available - guild.createMessageCommand(name) { - logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" } - this.nameLocalizations = nameLocalizations + guild.createMessageCommand(name) { + logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" } + this.nameLocalizations = nameLocalizations - this.register(locale, command) - } - } + this.register(locale, command) + } + } - return response.id - } + return response.id + } - // endregion + // endregion - // region: Extensions - /** Registration logic for slash commands, extracted for clarity. **/ - public open suspend fun ChatInputCreateBuilder.register(locale: Locale, command: SlashCommand<*, *, *>) { - if (this is GlobalChatInputCreateBuilder) { - registerGlobalPermissions(locale, command) - } else { - registerGuildPermissions(locale, command) - } + // region: Extensions + /** Registration logic for slash commands, extracted for clarity. **/ + public open suspend fun ChatInputCreateBuilder.register(locale: Locale, command: SlashCommand<*, *, *>) { + if (this is GlobalChatInputCreateBuilder) { + registerGlobalPermissions(locale, command) + } else { + registerGuildPermissions(locale, command) + } - if (command.hasBody) { - val args = command.arguments?.invoke() + if (command.hasBody) { + val args = command.arguments?.invoke() - if (args != null) { - args.args.forEach { arg -> - val converter = arg.converter + if (args != null) { + args.args.forEach { arg -> + val converter = arg.converter - if (converter !is SlashCommandConverter) { - error("Argument ${arg.displayName} does not support slash commands.") - } + if (converter !is SlashCommandConverter) { + error("Argument ${arg.displayName} does not support slash commands.") + } - if (this.options == null) this.options = mutableListOf() + if (this.options == null) this.options = mutableListOf() - val option = converter.toSlashOption(arg) + val option = converter.toSlashOption(arg) - option.translate(command, arg) + option.translate(command, arg) - if (option is BaseChoiceBuilder<*> && arg.converter.genericBuilder.autoCompleteCallback != null) { - option.choices?.clear() - } + if (option is BaseChoiceBuilder<*> && arg.converter.genericBuilder.autoCompleteCallback != null) { + option.choices?.clear() + } - option.autocomplete = arg.converter.genericBuilder.autoCompleteCallback != null + option.autocomplete = arg.converter.genericBuilder.autoCompleteCallback != null - this.options!! += option - } - } - } else { - command.subCommands.sortedByDescending { it.name }.forEach { - val args = it.arguments?.invoke()?.args?.map { arg -> - val converter = arg.converter + this.options!! += option + } + } + } else { + command.subCommands.sortedByDescending { it.name }.forEach { + val args = it.arguments?.invoke()?.args?.map { arg -> + val converter = arg.converter - if (converter !is SlashCommandConverter) { - error("Argument ${arg.displayName} does not support slash commands.") - } + if (converter !is SlashCommandConverter) { + error("Argument ${arg.displayName} does not support slash commands.") + } - val option = converter.toSlashOption(arg) - - option.translate(command, arg) + val option = converter.toSlashOption(arg) + + option.translate(command, arg) - if (option is BaseChoiceBuilder<*> && arg.converter.genericBuilder.autoCompleteCallback != null) { - option.choices?.clear() - } - - option.autocomplete = arg.converter.genericBuilder.autoCompleteCallback != null - - option - } - - val (name, nameLocalizations) = it.localizedName - val (description, descriptionLocalizations) = it.localizedDescription - - this.subCommand( - name, - description - ) { - this.nameLocalizations = nameLocalizations - this.descriptionLocalizations = descriptionLocalizations - - if (args != null) { - if (this.options == null) this.options = mutableListOf() - - this.options!!.addAll(args) - } - } - } - - command.groups.values.sortedByDescending { it.name }.forEach { group -> - val (name, nameLocalizations) = group.localizedName - val (description, descriptionLocalizations) = group.localizedDescription - - this.group(name, description) { - this.nameLocalizations = nameLocalizations - this.descriptionLocalizations = descriptionLocalizations - - group.subCommands.sortedByDescending { it.name }.forEach { - val args = it.arguments?.invoke()?.args?.map { arg -> - val converter = arg.converter - - if (converter !is SlashCommandConverter) { - error("Argument ${arg.displayName} does not support slash commands.") - } - - val option = converter.toSlashOption(arg) - - option.translate(command, arg) - - if ( - option is BaseChoiceBuilder<*> && - arg.converter.genericBuilder.autoCompleteCallback != null - ) { - option.choices?.clear() - } - - option.autocomplete = arg.converter.genericBuilder.autoCompleteCallback != null - - option - } - - val (name, nameLocalizations) = it.localizedName - val (description, descriptionLocalizations) = it.localizedDescription - - this.subCommand( - name, - description - ) { - this.nameLocalizations = nameLocalizations - this.descriptionLocalizations = descriptionLocalizations - - if (args != null) { - if (this.options == null) this.options = mutableListOf() - - this.options!!.addAll(args) - } - } - } - } - } - } - } - - /** Registration logic for message commands, extracted for clarity. **/ - @Suppress("UnusedPrivateMember") // Only for now... - public open fun MessageCommandCreateBuilder.register(locale: Locale, command: MessageCommand<*, *>) { - registerGuildPermissions(locale, command) - } - - /** Registration logic for user commands, extracted for clarity. **/ - @Suppress("UnusedPrivateMember") // Only for now... - public open fun UserCommandCreateBuilder.register(locale: Locale, command: UserCommand<*, *>) { - registerGuildPermissions(locale, command) - } - - /** Registration logic for message commands, extracted for clarity. **/ - @Suppress("UnusedPrivateMember") // Only for now... - public open fun GlobalMessageCommandCreateBuilder.register(locale: Locale, command: MessageCommand<*, *>) { - registerGuildPermissions(locale, command) - registerGlobalPermissions(locale, command) - } - - /** Registration logic for user commands, extracted for clarity. **/ - @Suppress("UnusedPrivateMember") // Only for now... - public open fun GlobalUserCommandCreateBuilder.register(locale: Locale, command: UserCommand<*, *>) { - registerGuildPermissions(locale, command) - registerGlobalPermissions(locale, command) - } - - /** - * Registers the global permissions of [command]. - */ - public open fun GlobalApplicationCommandCreateBuilder.registerGlobalPermissions( - locale: Locale, - command: ApplicationCommand<*>, - ) { - registerGuildPermissions(locale, command) - this.dmPermission = command.allowInDms - } - - /** - * Registers the guild permission of [command]. - */ - public open fun ApplicationCommandCreateBuilder.registerGuildPermissions( - locale: Locale, - command: ApplicationCommand<*>, - ) { - this.defaultMemberPermissions = command.defaultMemberPermissions - } - - /** Check whether the type and name of an extension-registered application command matches a Discord one. **/ - public open fun ApplicationCommand<*>.matches( - locale: Locale, - other: dev.kord.core.entity.application.ApplicationCommand, - ): Boolean = type == other.type && localizedName.default.equals(other.name, true) - - // endregion - - private fun OptionsBuilder.translate(command: ApplicationCommand<*>, argObj: Argument<*>) { - val defaultName = argObj.getDefaultTranslatedDisplayName(command.translationsProvider, command) - - if (defaultName != defaultName.lowercase(command.translationsProvider.defaultLocale)) { - throw InvalidNameException( - "Argument $name for command ${command.name} does not have a lower-case name in the configured " + - "default locale: ${command.translationsProvider.defaultLocale} -> $defaultName - this will " + - "cause issues with matching your command arguments to the options provided by users on Discord" - ) - } - - val (name, nameLocalizations) = command.localize(name, true) - - nameLocalizations.forEach { (locale, string) -> - if (string != string.lowercase(locale.asJavaLocale())) { - logger.warn { - "Argument $name for command ${command.name} is not lower-case in the ${locale.asJavaLocale()} " + - "locale: $string" - } - } - } - - this.name = name - this.nameLocalizations = nameLocalizations - - val (description, descriptionLocalizations) = command.localize(description) - - this.description = description - this.descriptionLocalizations = descriptionLocalizations - - if (this is BaseChoiceBuilder<*> && !choices.isNullOrEmpty()) { - translate(command) - } - } - - @Suppress("DEPRECATION_ERROR") - private fun BaseChoiceBuilder<*>.translate(command: ApplicationCommand<*>) { - choices = choices!!.map { - val (name, nameLocalizations) = command.localize(it.name) - - when (it) { - is Choice.NumberChoice -> Choice.NumberChoice(name, Optional(nameLocalizations), it.value) - is Choice.StringChoice -> Choice.StringChoice(name, Optional(nameLocalizations), it.value) - is Choice.IntegerChoice -> Choice.IntegerChoice(name, Optional(nameLocalizations), it.value) - } - }.toMutableList() - } + if (option is BaseChoiceBuilder<*> && arg.converter.genericBuilder.autoCompleteCallback != null) { + option.choices?.clear() + } + + option.autocomplete = arg.converter.genericBuilder.autoCompleteCallback != null + + option + } + + val (name, nameLocalizations) = it.localizedName + val (description, descriptionLocalizations) = it.localizedDescription + + this.subCommand( + name, + description + ) { + this.nameLocalizations = nameLocalizations + this.descriptionLocalizations = descriptionLocalizations + + if (args != null) { + if (this.options == null) this.options = mutableListOf() + + this.options!!.addAll(args) + } + } + } + + command.groups.values.sortedByDescending { it.name }.forEach { group -> + val (name, nameLocalizations) = group.localizedName + val (description, descriptionLocalizations) = group.localizedDescription + + this.group(name, description) { + this.nameLocalizations = nameLocalizations + this.descriptionLocalizations = descriptionLocalizations + + group.subCommands.sortedByDescending { it.name }.forEach { + val args = it.arguments?.invoke()?.args?.map { arg -> + val converter = arg.converter + + if (converter !is SlashCommandConverter) { + error("Argument ${arg.displayName} does not support slash commands.") + } + + val option = converter.toSlashOption(arg) + + option.translate(command, arg) + + if ( + option is BaseChoiceBuilder<*> && + arg.converter.genericBuilder.autoCompleteCallback != null + ) { + option.choices?.clear() + } + + option.autocomplete = arg.converter.genericBuilder.autoCompleteCallback != null + + option + } + + val (name, nameLocalizations) = it.localizedName + val (description, descriptionLocalizations) = it.localizedDescription + + this.subCommand( + name, + description + ) { + this.nameLocalizations = nameLocalizations + this.descriptionLocalizations = descriptionLocalizations + + if (args != null) { + if (this.options == null) this.options = mutableListOf() + + this.options!!.addAll(args) + } + } + } + } + } + } + } + + /** Registration logic for message commands, extracted for clarity. **/ + @Suppress("UnusedPrivateMember") // Only for now... + public open fun MessageCommandCreateBuilder.register(locale: Locale, command: MessageCommand<*, *>) { + registerGuildPermissions(locale, command) + } + + /** Registration logic for user commands, extracted for clarity. **/ + @Suppress("UnusedPrivateMember") // Only for now... + public open fun UserCommandCreateBuilder.register(locale: Locale, command: UserCommand<*, *>) { + registerGuildPermissions(locale, command) + } + + /** Registration logic for message commands, extracted for clarity. **/ + @Suppress("UnusedPrivateMember") // Only for now... + public open fun GlobalMessageCommandCreateBuilder.register(locale: Locale, command: MessageCommand<*, *>) { + registerGuildPermissions(locale, command) + registerGlobalPermissions(locale, command) + } + + /** Registration logic for user commands, extracted for clarity. **/ + @Suppress("UnusedPrivateMember") // Only for now... + public open fun GlobalUserCommandCreateBuilder.register(locale: Locale, command: UserCommand<*, *>) { + registerGuildPermissions(locale, command) + registerGlobalPermissions(locale, command) + } + + /** + * Registers the global permissions of [command]. + */ + public open fun GlobalApplicationCommandCreateBuilder.registerGlobalPermissions( + locale: Locale, + command: ApplicationCommand<*>, + ) { + registerGuildPermissions(locale, command) + this.dmPermission = command.allowInDms + } + + /** + * Registers the guild permission of [command]. + */ + public open fun ApplicationCommandCreateBuilder.registerGuildPermissions( + locale: Locale, + command: ApplicationCommand<*>, + ) { + this.defaultMemberPermissions = command.defaultMemberPermissions + } + + /** Check whether the type and name of an extension-registered application command matches a Discord one. **/ + public open fun ApplicationCommand<*>.matches( + locale: Locale, + other: dev.kord.core.entity.application.ApplicationCommand, + ): Boolean = type == other.type && localizedName.default.equals(other.name, true) + + // endregion + + private fun OptionsBuilder.translate(command: ApplicationCommand<*>, argObj: Argument<*>) { + val defaultName = argObj.getDefaultTranslatedDisplayName(command.translationsProvider, command) + + if (defaultName != defaultName.lowercase(command.translationsProvider.defaultLocale)) { + throw InvalidNameException( + "Argument $name for command ${command.name} does not have a lower-case name in the configured " + + "default locale: ${command.translationsProvider.defaultLocale} -> $defaultName - this will " + + "cause issues with matching your command arguments to the options provided by users on Discord" + ) + } + + val (name, nameLocalizations) = command.localize(name, true) + + nameLocalizations.forEach { (locale, string) -> + if (string != string.lowercase(locale.asJavaLocale())) { + logger.warn { + "Argument $name for command ${command.name} is not lower-case in the ${locale.asJavaLocale()} " + + "locale: $string" + } + } + } + + this.name = name + this.nameLocalizations = nameLocalizations + + val (description, descriptionLocalizations) = command.localize(description) + + this.description = description + this.descriptionLocalizations = descriptionLocalizations + + if (this is BaseChoiceBuilder<*> && !choices.isNullOrEmpty()) { + translate(command) + } + } + + @Suppress("DEPRECATION_ERROR") + private fun BaseChoiceBuilder<*>.translate(command: ApplicationCommand<*>) { + choices = choices!!.map { + val (name, nameLocalizations) = command.localize(it.name) + + when (it) { + is Choice.NumberChoice -> Choice.NumberChoice(name, Optional(nameLocalizations), it.value) + is Choice.StringChoice -> Choice.StringChoice(name, Optional(nameLocalizations), it.value) + is Choice.IntegerChoice -> Choice.IntegerChoice(name, Optional(nameLocalizations), it.value) + } + }.toMutableList() + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DefaultApplicationCommandRegistry.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DefaultApplicationCommandRegistry.kt index 7d035bceea..884ee83d8b 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DefaultApplicationCommandRegistry.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DefaultApplicationCommandRegistry.kt @@ -5,10 +5,10 @@ */ @file:Suppress( - "TooGenericExceptionCaught", - "StringLiteralDuplication", - "AnnotationSpacing", - "SpacingBetweenAnnotations" + "TooGenericExceptionCaught", + "StringLiteralDuplication", + "AnnotationSpacing", + "SpacingBetweenAnnotations" ) package com.kotlindiscord.kord.extensions.commands.application @@ -35,390 +35,390 @@ import kotlinx.coroutines.flow.toList /** Registry for all Discord application commands. **/ public open class DefaultApplicationCommandRegistry : ApplicationCommandRegistry() { - /** Mapping of Discord-side command ID to a message command object. **/ - public open val messageCommands: MutableMap> = mutableMapOf() - - /** Mapping of Discord-side command ID to a slash command object. **/ - public open val slashCommands: MutableMap> = mutableMapOf() - - /** Mapping of Discord-side command ID to a user command object. **/ - public open val userCommands: MutableMap> = mutableMapOf() - - public override suspend fun initialize(commands: List>) { - if (!bot.settings.applicationCommandsBuilder.register) { - logger.debug { - "Application command registration is disabled, pairing existing commands with extension commands" - } - } - - try { - syncAll(true, commands) - } catch (t: Throwable) { - logger.error(t) { "Failed to synchronise application commands" } - } - } - - // region: Untyped sync functions - - /** Register multiple generic application commands. **/ - public open suspend fun syncAll(removeOthers: Boolean = false, commands: List>) { - val groupedCommands = commands.groupBy { it.guildId }.toMutableMap() - - if (removeOthers && !groupedCommands.containsKey(null)) { - groupedCommands[null] = listOf() - } - - groupedCommands.forEach { - try { - sync(removeOthers, it.key, it.value) - } catch (e: KtorRequestException) { - logger.error(e) { - buildString { - if (it.key == null) { - append("Failed to synchronise global application commands") - } else { - append("Failed to synchronise application commands for guild with ID: ${it.key}") - } - - if (e.error?.message != null) { - append("\n Discord error message: ${e.error?.message}") - } - - if (e.error?.code == JsonErrorCode.MissingAccess) { - append( - "\n Double-check that the bot was added to this guild with the " + - "`application.commands` scope enabled" - ) - } - } - } - } catch (t: Throwable) { - logger.error(t) { - if (it.key == null) { - "Failed to synchronise global application commands" - } else { - "Failed to synchronise application commands for guild with ID: ${it.key}" - } - } - } - } - } - - /** Register multiple generic application commands. **/ - public open suspend fun sync( - removeOthers: Boolean = false, - guildId: Snowflake?, - commands: List>, - ) { - // NOTE: Someday, discord will make real i18n possible, we hope... - val locale = bot.settings.i18nBuilder.defaultLocale - - val guild = if (guildId != null) { - kord.getGuildOrNull(guildId) - ?: return logger.debug { - "Cannot register application commands for guild ID $guildId, " + - "as it seems to be missing." - } - } else { - null - } - - // Get guild commands if we're registering them (guild != null), otherwise get global commands - val registered = guild?.getApplicationCommands()?.toList() - ?: kord.getGlobalApplicationCommands().toList() - - if (!bot.settings.applicationCommandsBuilder.register) { - commands.forEach { commandObj -> - val existingCommand = registered.firstOrNull { commandObj.matches(locale, it) } - - if (existingCommand != null) { - when (commandObj) { - is MessageCommand<*, *> -> messageCommands[existingCommand.id] = commandObj - is SlashCommand<*, *, *> -> slashCommands[existingCommand.id] = commandObj - is UserCommand<*, *> -> userCommands[existingCommand.id] = commandObj - } - } - } - - return // We're only syncing them up, there's no other API work to do - } - - // Extension commands that haven't been registered yet - val toAdd = commands.filter { aC -> registered.all { dC -> !aC.matches(locale, dC) } } - - // Extension commands that were previously registered - val toUpdate = commands.filter { aC -> registered.any { dC -> aC.matches(locale, dC) } } - - // Registered Discord commands that haven't been provided by extensions - val toRemove = if (removeOthers) { - registered.filter { dC -> commands.all { aC -> !aC.matches(locale, dC) } } - } else { - listOf() - } - - logger.info { - buildString { - if (guild == null) { - append( - "Global application commands: ${toAdd.size} to add / " + - "${toUpdate.size} to update / " + - "${toRemove.size} to remove" - ) - } else { - append( - "Application commands for guild ${guild.name}: ${toAdd.size} to add / " + - "${toUpdate.size} to update / " + - "${toRemove.size} to remove" - ) - } - - if (!removeOthers) { - append("\nThe `removeOthers` parameter is `false`, so no commands will be removed.") - } - } - } - - val toCreate = toAdd + toUpdate - - val builder: suspend MultiApplicationCommandBuilder.() -> Unit = { - toCreate.forEach { - val (name, nameLocalizations) = it.localizedName - - logger.trace { "Adding/updating ${it.type.name} command: $name" } - - when (it) { - is MessageCommand<*, *> -> message(name) { - this.nameLocalizations = nameLocalizations - - this.register(locale, it) - } - - is UserCommand<*, *> -> user(name) { - this.nameLocalizations = nameLocalizations - - this.register(locale, it) - } - - is SlashCommand<*, *, *> -> { - val (description, descriptionLocalizations) = it.localizedDescription - - input(name, description) { - this.nameLocalizations = nameLocalizations - this.descriptionLocalizations = descriptionLocalizations - - this.register(locale, it) - } - } - } - } - } - - @Suppress("IfThenToElvis") // Ultimately, this is far more readable - val response = if (guild == null) { - // We're registering global commands here, if the guild is null - - kord.createGlobalApplicationCommands { builder() }.toList() - } else { - // We're registering guild-specific commands here, if the guild is available - guild.createApplicationCommands { builder() }.toList() - } - - // Next, we need to associate all the commands we just registered with the commands in our extensions - toCreate.forEach { command -> - val match = response.first { command.matches(locale, it) } - - when (command) { - is MessageCommand<*, *> -> messageCommands[match.id] = command - is SlashCommand<*, *, *> -> slashCommands[match.id] = command - is UserCommand<*, *> -> userCommands[match.id] = command - } - } - - if (toAdd.isEmpty() && toUpdate.isEmpty()) { - // Finally, we can remove anything that needs to be removed - toRemove.forEach { - logger.trace { "Removing ${it.type.name} command: ${it.name}" } - - @Suppress("MagicNumber") // not today, Detekt - try { - it.delete() - } catch (e: KtorRequestException) { - if (e.status.code != 404) { - throw e - } - } - } - } - - logger.info { - if (guild == null) { - "Finished synchronising global application commands" - } else { - "Finished synchronising application commands for guild ${guild.name}" - } - } - } + /** Mapping of Discord-side command ID to a message command object. **/ + public open val messageCommands: MutableMap> = mutableMapOf() + + /** Mapping of Discord-side command ID to a slash command object. **/ + public open val slashCommands: MutableMap> = mutableMapOf() + + /** Mapping of Discord-side command ID to a user command object. **/ + public open val userCommands: MutableMap> = mutableMapOf() + + public override suspend fun initialize(commands: List>) { + if (!bot.settings.applicationCommandsBuilder.register) { + logger.debug { + "Application command registration is disabled, pairing existing commands with extension commands" + } + } + + try { + syncAll(true, commands) + } catch (t: Throwable) { + logger.error(t) { "Failed to synchronise application commands" } + } + } + + // region: Untyped sync functions + + /** Register multiple generic application commands. **/ + public open suspend fun syncAll(removeOthers: Boolean = false, commands: List>) { + val groupedCommands = commands.groupBy { it.guildId }.toMutableMap() + + if (removeOthers && !groupedCommands.containsKey(null)) { + groupedCommands[null] = listOf() + } + + groupedCommands.forEach { + try { + sync(removeOthers, it.key, it.value) + } catch (e: KtorRequestException) { + logger.error(e) { + buildString { + if (it.key == null) { + append("Failed to synchronise global application commands") + } else { + append("Failed to synchronise application commands for guild with ID: ${it.key}") + } + + if (e.error?.message != null) { + append("\n Discord error message: ${e.error?.message}") + } + + if (e.error?.code == JsonErrorCode.MissingAccess) { + append( + "\n Double-check that the bot was added to this guild with the " + + "`application.commands` scope enabled" + ) + } + } + } + } catch (t: Throwable) { + logger.error(t) { + if (it.key == null) { + "Failed to synchronise global application commands" + } else { + "Failed to synchronise application commands for guild with ID: ${it.key}" + } + } + } + } + } + + /** Register multiple generic application commands. **/ + public open suspend fun sync( + removeOthers: Boolean = false, + guildId: Snowflake?, + commands: List>, + ) { + // NOTE: Someday, discord will make real i18n possible, we hope... + val locale = bot.settings.i18nBuilder.defaultLocale + + val guild = if (guildId != null) { + kord.getGuildOrNull(guildId) + ?: return logger.debug { + "Cannot register application commands for guild ID $guildId, " + + "as it seems to be missing." + } + } else { + null + } + + // Get guild commands if we're registering them (guild != null), otherwise get global commands + val registered = guild?.getApplicationCommands()?.toList() + ?: kord.getGlobalApplicationCommands().toList() + + if (!bot.settings.applicationCommandsBuilder.register) { + commands.forEach { commandObj -> + val existingCommand = registered.firstOrNull { commandObj.matches(locale, it) } + + if (existingCommand != null) { + when (commandObj) { + is MessageCommand<*, *> -> messageCommands[existingCommand.id] = commandObj + is SlashCommand<*, *, *> -> slashCommands[existingCommand.id] = commandObj + is UserCommand<*, *> -> userCommands[existingCommand.id] = commandObj + } + } + } + + return // We're only syncing them up, there's no other API work to do + } + + // Extension commands that haven't been registered yet + val toAdd = commands.filter { aC -> registered.all { dC -> !aC.matches(locale, dC) } } + + // Extension commands that were previously registered + val toUpdate = commands.filter { aC -> registered.any { dC -> aC.matches(locale, dC) } } + + // Registered Discord commands that haven't been provided by extensions + val toRemove = if (removeOthers) { + registered.filter { dC -> commands.all { aC -> !aC.matches(locale, dC) } } + } else { + listOf() + } + + logger.info { + buildString { + if (guild == null) { + append( + "Global application commands: ${toAdd.size} to add / " + + "${toUpdate.size} to update / " + + "${toRemove.size} to remove" + ) + } else { + append( + "Application commands for guild ${guild.name}: ${toAdd.size} to add / " + + "${toUpdate.size} to update / " + + "${toRemove.size} to remove" + ) + } + + if (!removeOthers) { + append("\nThe `removeOthers` parameter is `false`, so no commands will be removed.") + } + } + } + + val toCreate = toAdd + toUpdate + + val builder: suspend MultiApplicationCommandBuilder.() -> Unit = { + toCreate.forEach { + val (name, nameLocalizations) = it.localizedName + + logger.trace { "Adding/updating ${it.type.name} command: $name" } + + when (it) { + is MessageCommand<*, *> -> message(name) { + this.nameLocalizations = nameLocalizations + + this.register(locale, it) + } + + is UserCommand<*, *> -> user(name) { + this.nameLocalizations = nameLocalizations + + this.register(locale, it) + } + + is SlashCommand<*, *, *> -> { + val (description, descriptionLocalizations) = it.localizedDescription + + input(name, description) { + this.nameLocalizations = nameLocalizations + this.descriptionLocalizations = descriptionLocalizations + + this.register(locale, it) + } + } + } + } + } + + @Suppress("IfThenToElvis") // Ultimately, this is far more readable + val response = if (guild == null) { + // We're registering global commands here, if the guild is null + + kord.createGlobalApplicationCommands { builder() }.toList() + } else { + // We're registering guild-specific commands here, if the guild is available + guild.createApplicationCommands { builder() }.toList() + } + + // Next, we need to associate all the commands we just registered with the commands in our extensions + toCreate.forEach { command -> + val match = response.first { command.matches(locale, it) } + + when (command) { + is MessageCommand<*, *> -> messageCommands[match.id] = command + is SlashCommand<*, *, *> -> slashCommands[match.id] = command + is UserCommand<*, *> -> userCommands[match.id] = command + } + } + + if (toAdd.isEmpty() && toUpdate.isEmpty()) { + // Finally, we can remove anything that needs to be removed + toRemove.forEach { + logger.trace { "Removing ${it.type.name} command: ${it.name}" } + + @Suppress("MagicNumber") // not today, Detekt + try { + it.delete() + } catch (e: KtorRequestException) { + if (e.status.code != 404) { + throw e + } + } + } + } + + logger.info { + if (guild == null) { + "Finished synchronising global application commands" + } else { + "Finished synchronising application commands for guild ${guild.name}" + } + } + } - // endregion + // endregion - // region: Typed registration functions + // region: Typed registration functions - /** Register a message command. **/ - public override suspend fun register(command: MessageCommand<*, *>): MessageCommand<*, *>? { - val commandId = createDiscordCommand(command) ?: return null + /** Register a message command. **/ + public override suspend fun register(command: MessageCommand<*, *>): MessageCommand<*, *>? { + val commandId = createDiscordCommand(command) ?: return null - messageCommands[commandId] = command + messageCommands[commandId] = command - return command - } + return command + } - /** Register a slash command. **/ - public override suspend fun register(command: SlashCommand<*, *, *>): SlashCommand<*, *, *>? { - val commandId = createDiscordCommand(command) ?: return null + /** Register a slash command. **/ + public override suspend fun register(command: SlashCommand<*, *, *>): SlashCommand<*, *, *>? { + val commandId = createDiscordCommand(command) ?: return null - slashCommands[commandId] = command + slashCommands[commandId] = command - return command - } + return command + } - /** Register a user command. **/ - public override suspend fun register(command: UserCommand<*, *>): UserCommand<*, *>? { - val commandId = createDiscordCommand(command) ?: return null + /** Register a user command. **/ + public override suspend fun register(command: UserCommand<*, *>): UserCommand<*, *>? { + val commandId = createDiscordCommand(command) ?: return null - userCommands[commandId] = command + userCommands[commandId] = command - return command - } + return command + } - // endregion + // endregion - // region: Unregistration functions + // region: Unregistration functions - /** Unregister a message command. **/ - public override suspend fun unregister(command: MessageCommand<*, *>, delete: Boolean): MessageCommand<*, *>? { - val filtered = messageCommands.filter { it.value == command } - val id = filtered.keys.firstOrNull() ?: return null + /** Unregister a message command. **/ + public override suspend fun unregister(command: MessageCommand<*, *>, delete: Boolean): MessageCommand<*, *>? { + val filtered = messageCommands.filter { it.value == command } + val id = filtered.keys.firstOrNull() ?: return null - if (delete) { - deleteGeneric(command, id) - } + if (delete) { + deleteGeneric(command, id) + } - return messageCommands.remove(id) - } + return messageCommands.remove(id) + } - /** Unregister a slash command. **/ - public override suspend fun unregister(command: SlashCommand<*, *, *>, delete: Boolean): SlashCommand<*, *, *>? { - val filtered = slashCommands.filter { it.value == command } - val id = filtered.keys.firstOrNull() ?: return null + /** Unregister a slash command. **/ + public override suspend fun unregister(command: SlashCommand<*, *, *>, delete: Boolean): SlashCommand<*, *, *>? { + val filtered = slashCommands.filter { it.value == command } + val id = filtered.keys.firstOrNull() ?: return null - if (delete) { - deleteGeneric(command, id) - } + if (delete) { + deleteGeneric(command, id) + } - return slashCommands.remove(id) - } + return slashCommands.remove(id) + } - /** Unregister a user command. **/ - public override suspend fun unregister(command: UserCommand<*, *>, delete: Boolean): UserCommand<*, *>? { - val filtered = userCommands.filter { it.value == command } - val id = filtered.keys.firstOrNull() ?: return null + /** Unregister a user command. **/ + public override suspend fun unregister(command: UserCommand<*, *>, delete: Boolean): UserCommand<*, *>? { + val filtered = userCommands.filter { it.value == command } + val id = filtered.keys.firstOrNull() ?: return null - if (delete) { - deleteGeneric(command, id) - } + if (delete) { + deleteGeneric(command, id) + } - return userCommands.remove(id) - } + return userCommands.remove(id) + } - // endregion + // endregion - // region: Event handlers + // region: Event handlers - /** Event handler for message commands. **/ - public override suspend fun handle(event: MessageCommandInteractionCreateEvent) { - val commandId = event.interaction.invokedCommandId - val command = messageCommands[commandId] + /** Event handler for message commands. **/ + public override suspend fun handle(event: MessageCommandInteractionCreateEvent) { + val commandId = event.interaction.invokedCommandId + val command = messageCommands[commandId] - command ?: return logger.warn { "Received interaction for unknown message command: $commandId" } + command ?: return logger.warn { "Received interaction for unknown message command: $commandId" } - command.doCall(event) - } + command.doCall(event) + } - /** Event handler for slash commands. **/ - public override suspend fun handle(event: ChatInputCommandInteractionCreateEvent) { - val commandId = event.interaction.command.rootId - val command = slashCommands[commandId] + /** Event handler for slash commands. **/ + public override suspend fun handle(event: ChatInputCommandInteractionCreateEvent) { + val commandId = event.interaction.command.rootId + val command = slashCommands[commandId] - command ?: return logger.warn { "Received interaction for unknown slash command: $commandId" } + command ?: return logger.warn { "Received interaction for unknown slash command: $commandId" } - command.doCall(event) - } + command.doCall(event) + } - /** Event handler for user commands. **/ - public override suspend fun handle(event: UserCommandInteractionCreateEvent) { - val commandId = event.interaction.invokedCommandId - val command = userCommands[commandId] + /** Event handler for user commands. **/ + public override suspend fun handle(event: UserCommandInteractionCreateEvent) { + val commandId = event.interaction.invokedCommandId + val command = userCommands[commandId] - command ?: return logger.warn { "Received interaction for unknown user command: $commandId" } + command ?: return logger.warn { "Received interaction for unknown user command: $commandId" } - command.doCall(event) - } + command.doCall(event) + } - override suspend fun handle(event: AutoCompleteInteractionCreateEvent) { - val commandId = event.interaction.command.rootId - val command = slashCommands[commandId]?.findCommand(event) + override suspend fun handle(event: AutoCompleteInteractionCreateEvent) { + val commandId = event.interaction.command.rootId + val command = slashCommands[commandId]?.findCommand(event) - command ?: return logger.warn { "Received autocomplete interaction for unknown command: $commandId" } + command ?: return logger.warn { "Received autocomplete interaction for unknown command: $commandId" } - if (command.arguments == null) { - return logger.trace { "Command $command doesn't have any arguments." } - } + if (command.arguments == null) { + return logger.trace { "Command $command doesn't have any arguments." } + } - val option = event.interaction.command.options.filterValues { it.focused }.toList().firstOrNull() + val option = event.interaction.command.options.filterValues { it.focused }.toList().firstOrNull() - option ?: return logger.trace { "Autocomplete event for command $command doesn't have a focused option." } + option ?: return logger.trace { "Autocomplete event for command $command doesn't have a focused option." } - val arguments = command.arguments!!() + val arguments = command.arguments!!() - val arg = arguments.args.firstOrNull { - it.getDefaultTranslatedDisplayName(translationsProvider, command) == option.first - } + val arg = arguments.args.firstOrNull { + it.getDefaultTranslatedDisplayName(translationsProvider, command) == option.first + } - arg ?: return logger.warn { - "Autocomplete event for command $command has an unknown focused option: ${option.first}." - } + arg ?: return logger.warn { + "Autocomplete event for command $command has an unknown focused option: ${option.first}." + } - val callback = arg.converter.genericBuilder.autoCompleteCallback + val callback = arg.converter.genericBuilder.autoCompleteCallback - callback ?: return logger.trace { - "Autocomplete event for command $command has an focused option without a callback: ${option.first}." - } + callback ?: return logger.trace { + "Autocomplete event for command $command has an focused option without a callback: ${option.first}." + } - if (arguments.parseForAutocomplete) { - val context = DummyAutocompleteCommandContext(command, event, command.name) + if (arguments.parseForAutocomplete) { + val context = DummyAutocompleteCommandContext(command, event, command.name) - for (priorArg in arguments.args) { - if (priorArg == arg) { - break - } + for (priorArg in arguments.args) { + if (priorArg == arg) { + break + } - val argName = priorArg.getDefaultTranslatedDisplayName(translationsProvider, command) - val currentOption = event.interaction.command.options[argName] + val argName = priorArg.getDefaultTranslatedDisplayName(translationsProvider, command) + val currentOption = event.interaction.command.options[argName] - if (currentOption == null) { - continue - } + if (currentOption == null) { + continue + } - try { - (priorArg.converter as SlashCommandConverter).parseOption(context, currentOption) - } catch (e: Exception) { - logger.error(e) { "Failed to parse option $argName" } - } - } - } + try { + (priorArg.converter as SlashCommandConverter).parseOption(context, currentOption) + } catch (e: Exception) { + logger.error(e) { "Failed to parse option $argName" } + } + } + } - callback(event.interaction, event) - } + callback(event.interaction, event) + } - // endregion + // endregion } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DummyAutocompleteCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DummyAutocompleteCommandContext.kt index d641ac073f..a65ea8f2dd 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DummyAutocompleteCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DummyAutocompleteCommandContext.kt @@ -10,23 +10,23 @@ import dev.kord.core.entity.interaction.GuildAutoCompleteInteraction import dev.kord.core.event.interaction.AutoCompleteInteractionCreateEvent public class DummyAutocompleteCommandContext( - command: Command, - private val event: AutoCompleteInteractionCreateEvent, - commandName: String, + command: Command, + private val event: AutoCompleteInteractionCreateEvent, + commandName: String, ) : CommandContext(command, event, commandName, mutableMapOf()) { - override suspend fun populate() { - error("This should never be called.") - } + override suspend fun populate() { + error("This should never be called.") + } - override suspend fun getChannel(): ChannelBehavior = - event.interaction.channel + override suspend fun getChannel(): ChannelBehavior = + event.interaction.channel - override suspend fun getGuild(): GuildBehavior? = - (event.interaction as? GuildAutoCompleteInteraction)?.guild + override suspend fun getGuild(): GuildBehavior? = + (event.interaction as? GuildAutoCompleteInteraction)?.guild - override suspend fun getMember(): MemberBehavior? = - (event.interaction as? GuildAutoCompleteInteraction)?.user?.asMemberOrNull() + override suspend fun getMember(): MemberBehavior? = + (event.interaction as? GuildAutoCompleteInteraction)?.user?.asMemberOrNull() - override suspend fun getUser(): UserBehavior = - event.interaction.user + override suspend fun getUser(): UserBehavior = + event.interaction.user } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/StorageAwareApplicationCommandRegistry.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/StorageAwareApplicationCommandRegistry.kt index c67ce63c19..9c2f3517a6 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/StorageAwareApplicationCommandRegistry.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/StorageAwareApplicationCommandRegistry.kt @@ -28,173 +28,173 @@ import kotlinx.coroutines.flow.toList * Discord lifecycles may not be implemented in this class and require manual updating. */ public open class StorageAwareApplicationCommandRegistry( - builder: () -> RegistryStorage>, + builder: () -> RegistryStorage>, ) : ApplicationCommandRegistry() { - protected open val commandRegistry: RegistryStorage> = builder.invoke() + protected open val commandRegistry: RegistryStorage> = builder.invoke() - override suspend fun initialize(commands: List>) { - commands.forEach { commandRegistry.register(it) } + override suspend fun initialize(commands: List>) { + commands.forEach { commandRegistry.register(it) } - val registeredCommands = commandRegistry.entryFlow().toList() + val registeredCommands = commandRegistry.entryFlow().toList() - commands.forEach { command -> - if (registeredCommands.none { it.hasCommand(command) }) { - val commandId = createDiscordCommand(command) + commands.forEach { command -> + if (registeredCommands.none { it.hasCommand(command) }) { + val commandId = createDiscordCommand(command) - commandId?.let { - commandRegistry.set(it, command) - } - } - } - } + commandId?.let { + commandRegistry.set(it, command) + } + } + } + } - override suspend fun register(command: SlashCommand<*, *, *>): SlashCommand<*, *, *>? { - val commandId = createDiscordCommand(command) ?: return null + override suspend fun register(command: SlashCommand<*, *, *>): SlashCommand<*, *, *>? { + val commandId = createDiscordCommand(command) ?: return null - commandRegistry.set(commandId, command) + commandRegistry.set(commandId, command) - return command - } + return command + } - override suspend fun register(command: MessageCommand<*, *>): MessageCommand<*, *>? { - val commandId = createDiscordCommand(command) ?: return null + override suspend fun register(command: MessageCommand<*, *>): MessageCommand<*, *>? { + val commandId = createDiscordCommand(command) ?: return null - commandRegistry.set(commandId, command) + commandRegistry.set(commandId, command) - return command - } + return command + } - override suspend fun register(command: UserCommand<*, *>): UserCommand<*, *>? { - val commandId = createDiscordCommand(command) ?: return null + override suspend fun register(command: UserCommand<*, *>): UserCommand<*, *>? { + val commandId = createDiscordCommand(command) ?: return null - commandRegistry.set(commandId, command) + commandRegistry.set(commandId, command) - return command - } + return command + } - override suspend fun handle(event: ChatInputCommandInteractionCreateEvent) { - val commandId = event.interaction.invokedCommandId - val command = commandRegistry.get(commandId) as? SlashCommand<*, *, *> + override suspend fun handle(event: ChatInputCommandInteractionCreateEvent) { + val commandId = event.interaction.invokedCommandId + val command = commandRegistry.get(commandId) as? SlashCommand<*, *, *> - command ?: return logger.warn { "Received interaction for unknown slash command: $commandId" } + command ?: return logger.warn { "Received interaction for unknown slash command: $commandId" } - command.doCall(event) - } + command.doCall(event) + } - override suspend fun handle(event: MessageCommandInteractionCreateEvent) { - val commandId = event.interaction.invokedCommandId - val command = commandRegistry.get(commandId) as? MessageCommand<*, *> + override suspend fun handle(event: MessageCommandInteractionCreateEvent) { + val commandId = event.interaction.invokedCommandId + val command = commandRegistry.get(commandId) as? MessageCommand<*, *> - command ?: return logger.warn { "Received interaction for unknown message command: $commandId" } + command ?: return logger.warn { "Received interaction for unknown message command: $commandId" } - command.doCall(event) - } + command.doCall(event) + } - override suspend fun handle(event: UserCommandInteractionCreateEvent) { - val commandId = event.interaction.invokedCommandId - val command = commandRegistry.get(commandId) as? UserCommand<*, *> + override suspend fun handle(event: UserCommandInteractionCreateEvent) { + val commandId = event.interaction.invokedCommandId + val command = commandRegistry.get(commandId) as? UserCommand<*, *> - command ?: return logger.warn { "Received interaction for unknown user command: $commandId" } + command ?: return logger.warn { "Received interaction for unknown user command: $commandId" } - command.doCall(event) - } + command.doCall(event) + } - override suspend fun handle(event: AutoCompleteInteractionCreateEvent) { - val commandId = event.interaction.command.rootId - val command = commandRegistry.get(commandId) as? SlashCommand<*, *, *> + override suspend fun handle(event: AutoCompleteInteractionCreateEvent) { + val commandId = event.interaction.command.rootId + val command = commandRegistry.get(commandId) as? SlashCommand<*, *, *> - command ?: return logger.warn { "Received autocomplete interaction for unknown command: $commandId" } + command ?: return logger.warn { "Received autocomplete interaction for unknown command: $commandId" } - if (command.arguments == null) { - return logger.trace { "Command $command doesn't have any arguments." } - } + if (command.arguments == null) { + return logger.trace { "Command $command doesn't have any arguments." } + } - val option = event.interaction.command.options.filterValues { it.focused }.toList().firstOrNull() + val option = event.interaction.command.options.filterValues { it.focused }.toList().firstOrNull() - option ?: return logger.trace { "Autocomplete event for command $command doesn't have a focused option." } + option ?: return logger.trace { "Autocomplete event for command $command doesn't have a focused option." } - val arguments = command.arguments!!() + val arguments = command.arguments!!() - val arg = arguments.args.firstOrNull { - it.getDefaultTranslatedDisplayName( - translationsProvider, - command - ) == option.first - } + val arg = arguments.args.firstOrNull { + it.getDefaultTranslatedDisplayName( + translationsProvider, + command + ) == option.first + } - arg ?: return logger.warn { - "Autocomplete event for command $command has an unknown focused option: ${option.first}." - } + arg ?: return logger.warn { + "Autocomplete event for command $command has an unknown focused option: ${option.first}." + } - val callback = arg.converter.genericBuilder.autoCompleteCallback + val callback = arg.converter.genericBuilder.autoCompleteCallback - callback ?: return logger.trace { - "Autocomplete event for command $command has an focused option without a callback: ${option.first}." - } + callback ?: return logger.trace { + "Autocomplete event for command $command has an focused option without a callback: ${option.first}." + } - if (arguments.parseForAutocomplete) { - val context = DummyAutocompleteCommandContext(command, event, command.name) + if (arguments.parseForAutocomplete) { + val context = DummyAutocompleteCommandContext(command, event, command.name) - for (priorArg in arguments.args) { - if (priorArg == arg) { - break - } + for (priorArg in arguments.args) { + if (priorArg == arg) { + break + } - val argName = priorArg.getDefaultTranslatedDisplayName(translationsProvider, command) - val currentOption = event.interaction.command.options[argName] + val argName = priorArg.getDefaultTranslatedDisplayName(translationsProvider, command) + val currentOption = event.interaction.command.options[argName] - if (currentOption == null) { - continue - } + if (currentOption == null) { + continue + } - try { - (priorArg.converter as SlashCommandConverter).parseOption(context, currentOption) - } catch (e: Exception) { - logger.error(e) { "Failed to parse option $argName" } - } - } - } + try { + (priorArg.converter as SlashCommandConverter).parseOption(context, currentOption) + } catch (e: Exception) { + logger.error(e) { "Failed to parse option $argName" } + } + } + } - callback(event.interaction, event) - } + callback(event.interaction, event) + } - override suspend fun unregister(command: SlashCommand<*, *, *>, delete: Boolean): SlashCommand<*, *, *>? = - unregisterApplicationCommand(command, delete) as? SlashCommand<*, *, *> + override suspend fun unregister(command: SlashCommand<*, *, *>, delete: Boolean): SlashCommand<*, *, *>? = + unregisterApplicationCommand(command, delete) as? SlashCommand<*, *, *> - override suspend fun unregister(command: MessageCommand<*, *>, delete: Boolean): MessageCommand<*, *>? = - unregisterApplicationCommand(command, delete) as? MessageCommand<*, *> + override suspend fun unregister(command: MessageCommand<*, *>, delete: Boolean): MessageCommand<*, *>? = + unregisterApplicationCommand(command, delete) as? MessageCommand<*, *> - override suspend fun unregister(command: UserCommand<*, *>, delete: Boolean): UserCommand<*, *>? = - unregisterApplicationCommand(command, delete) as? UserCommand<*, *> + override suspend fun unregister(command: UserCommand<*, *>, delete: Boolean): UserCommand<*, *>? = + unregisterApplicationCommand(command, delete) as? UserCommand<*, *> - protected open suspend fun unregisterApplicationCommand( - command: ApplicationCommand<*>, - delete: Boolean, - ): ApplicationCommand<*>? { - val id = commandRegistry.constructUniqueIdentifier(command) + protected open suspend fun unregisterApplicationCommand( + command: ApplicationCommand<*>, + delete: Boolean, + ): ApplicationCommand<*>? { + val id = commandRegistry.constructUniqueIdentifier(command) - val snowflake = commandRegistry.entryFlow() - .firstOrNull { commandRegistry.constructUniqueIdentifier(it.value) == id } - ?.key + val snowflake = commandRegistry.entryFlow() + .firstOrNull { commandRegistry.constructUniqueIdentifier(it.value) == id } + ?.key - snowflake?.let { - if (delete) { - deleteGeneric(command, it) - } + snowflake?.let { + if (delete) { + deleteGeneric(command, it) + } - return commandRegistry.remove(it) - } + return commandRegistry.remove(it) + } - return null - } + return null + } - protected open fun RegistryStorage.StorageEntry>.hasCommand( - command: ApplicationCommand<*>, - ): Boolean { - val key = commandRegistry.constructUniqueIdentifier(value) - val other = commandRegistry.constructUniqueIdentifier(command) + protected open fun RegistryStorage.StorageEntry>.hasCommand( + command: ApplicationCommand<*>, + ): Boolean { + val key = commandRegistry.constructUniqueIdentifier(value) + val other = commandRegistry.constructUniqueIdentifier(command) - return key == other - } + return key == other + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/EphemeralMessageCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/EphemeralMessageCommand.kt index 5705891da6..79c8308299 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/EphemeralMessageCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/EphemeralMessageCommand.kt @@ -27,122 +27,122 @@ import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialEphemeralMessageResponseBuilder = - (suspend InteractionResponseCreateBuilder.(MessageCommandInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(MessageCommandInteractionCreateEvent) -> Unit)? /** Ephemeral message command. **/ public class EphemeralMessageCommand( - extension: Extension, - public override val modal: (() -> M)? = null, + extension: Extension, + public override val modal: (() -> M)? = null, ) : MessageCommand, M>(extension) { - /** @suppress Internal guilder **/ - public var initialResponseBuilder: InitialEphemeralMessageResponseBuilder = null + /** @suppress Internal guilder **/ + public var initialResponseBuilder: InitialEphemeralMessageResponseBuilder = null - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialEphemeralMessageResponseBuilder) { - initialResponseBuilder = body - } + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialEphemeralMessageResponseBuilder) { + initialResponseBuilder = body + } - override fun validate() { - super.validate() + override fun validate() { + super.validate() - if (modal != null && initialResponseBuilder != null) { - throw InvalidCommandException( - name, + if (modal != null && initialResponseBuilder != null) { + throw InvalidCommandException( + name, - "You may not provide a modal builder and an initial response - pick one, not both." - ) - } - } + "You may not provide a modal builder and an initial response - pick one, not both." + ) + } + } - override suspend fun call(event: MessageCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { - emitEventAsync(EphemeralMessageCommandInvocationEvent(this, event)) + override suspend fun call(event: MessageCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { + emitEventAsync(EphemeralMessageCommandInvocationEvent(this, event)) - try { - if (!runChecks(event, cache)) { - emitEventAsync( - EphemeralMessageCommandFailedChecksEvent( - this, - event, - "Checks failed without a message." - ) - ) + try { + if (!runChecks(event, cache)) { + emitEventAsync( + EphemeralMessageCommandFailedChecksEvent( + this, + event, + "Checks failed without a message." + ) + ) - return - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } + return + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } - emitEventAsync(EphemeralMessageCommandFailedChecksEvent(this, event, e.reason)) + emitEventAsync(EphemeralMessageCommandFailedChecksEvent(this, event, e.reason)) - return - } + return + } - val modalObj = modal?.invoke() + val modalObj = modal?.invoke() - val response = if (initialResponseBuilder != null) { - event.interaction.respondEphemeral { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) + val response = if (initialResponseBuilder != null) { + event.interaction.respondEphemeral { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) - val locale = event.getLocale() + val locale = event.getLocale() - event.interaction.modal( - modalObj.translateTitle(locale, resolvedBundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) - } + event.interaction.modal( + modalObj.translateTitle(locale, resolvedBundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) + } - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) - it?.deferEphemeralResponseUnsafe() - } ?: return - } else { - event.interaction.deferEphemeralResponseUnsafe() - } + it?.deferEphemeralResponseUnsafe() + } ?: return + } else { + event.interaction.deferEphemeralResponseUnsafe() + } - val context = EphemeralMessageCommandContext(event, this, response, cache) + val context = EphemeralMessageCommandContext(event, this, response, cache) - context.populate() + context.populate() - firstSentryBreadcrumb(context) + firstSentryBreadcrumb(context) - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - emitEventAsync(EphemeralMessageCommandFailedChecksEvent(this, event, e.reason)) + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + emitEventAsync(EphemeralMessageCommandFailedChecksEvent(this, event, e.reason)) - return - } + return + } - try { - body(context, modalObj) - } catch (t: Throwable) { - emitEventAsync(EphemeralMessageCommandFailedWithExceptionEvent(this, event, t)) + try { + body(context, modalObj) + } catch (t: Throwable) { + emitEventAsync(EphemeralMessageCommandFailedWithExceptionEvent(this, event, t)) - if (t is DiscordRelayedException) { - respondText(context, t.reason, FailureReason.RelayedFailure(t)) + if (t is DiscordRelayedException) { + respondText(context, t.reason, FailureReason.RelayedFailure(t)) - return - } + return + } - handleError(context, t) + handleError(context, t) - return - } + return + } - emitEventAsync(EphemeralMessageCommandSucceededEvent(this, event)) - } - - override suspend fun respondText( - context: EphemeralMessageCommandContext, - message: String, - failureType: FailureReason<*> - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + emitEventAsync(EphemeralMessageCommandSucceededEvent(this, event)) + } + + override suspend fun respondText( + context: EphemeralMessageCommandContext, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/EphemeralMessageCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/EphemeralMessageCommandContext.kt index 8181dbbc39..c499593637 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/EphemeralMessageCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/EphemeralMessageCommandContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent /** Ephemeral-only message command context. **/ public class EphemeralMessageCommandContext( - override val event: MessageCommandInteractionCreateEvent, - override val command: MessageCommand, M>, - override val interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val event: MessageCommandInteractionCreateEvent, + override val command: MessageCommand, M>, + override val interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : MessageCommandContext, M>(event, command, cache), EphemeralInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/MessageCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/MessageCommandContext.kt index 0a70314a35..8cca45e377 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/MessageCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/MessageCommandContext.kt @@ -19,10 +19,10 @@ import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent * @param command Message command instance. */ public abstract class MessageCommandContext, M : ModalForm>( - public open val event: MessageCommandInteractionCreateEvent, - public override val command: MessageCommand, - cache: MutableStringKeyedMap + public open val event: MessageCommandInteractionCreateEvent, + public override val command: MessageCommand, + cache: MutableStringKeyedMap, ) : ApplicationCommandContext(event, command, cache) { - /** Messages that this message command is being executed against. **/ - public val targetMessages: Collection by lazy { event.interaction.messages.values } + /** Messages that this message command is being executed against. **/ + public val targetMessages: Collection by lazy { event.interaction.messages.values } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/PublicMessageCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/PublicMessageCommand.kt index 96b4b7f672..a190019d2c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/PublicMessageCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/PublicMessageCommand.kt @@ -28,122 +28,122 @@ import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialPublicMessageResponseBuilder = - (suspend InteractionResponseCreateBuilder.(MessageCommandInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(MessageCommandInteractionCreateEvent) -> Unit)? /** Public message command. **/ public class PublicMessageCommand( - extension: Extension, - public override val modal: (() -> M)? = null, + extension: Extension, + public override val modal: (() -> M)? = null, ) : MessageCommand, M>(extension) { - /** @suppress Internal guilder **/ - public var initialResponseBuilder: InitialPublicMessageResponseBuilder = null + /** @suppress Internal guilder **/ + public var initialResponseBuilder: InitialPublicMessageResponseBuilder = null - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialPublicMessageResponseBuilder) { - initialResponseBuilder = body - } + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialPublicMessageResponseBuilder) { + initialResponseBuilder = body + } - override fun validate() { - super.validate() + override fun validate() { + super.validate() - if (modal != null && initialResponseBuilder != null) { - throw InvalidCommandException( - name, + if (modal != null && initialResponseBuilder != null) { + throw InvalidCommandException( + name, - "You may not provide a modal builder and an initial response - pick one, not both." - ) - } - } + "You may not provide a modal builder and an initial response - pick one, not both." + ) + } + } - override suspend fun call(event: MessageCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { - emitEventAsync(PublicMessageCommandInvocationEvent(this, event)) + override suspend fun call(event: MessageCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { + emitEventAsync(PublicMessageCommandInvocationEvent(this, event)) - try { - if (!runChecks(event, cache)) { - emitEventAsync( - PublicMessageCommandFailedChecksEvent( - this, - event, - "Checks failed without a message." - ) - ) + try { + if (!runChecks(event, cache)) { + emitEventAsync( + PublicMessageCommandFailedChecksEvent( + this, + event, + "Checks failed without a message." + ) + ) - return - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } + return + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } - emitEventAsync(PublicMessageCommandFailedChecksEvent(this, event, e.reason)) + emitEventAsync(PublicMessageCommandFailedChecksEvent(this, event, e.reason)) - return - } + return + } - val modalObj = modal?.invoke() + val modalObj = modal?.invoke() - val response = if (initialResponseBuilder != null) { - event.interaction.respondPublic { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) + val response = if (initialResponseBuilder != null) { + event.interaction.respondPublic { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) - val locale = event.getLocale() + val locale = event.getLocale() - event.interaction.modal( - modalObj.translateTitle(locale, resolvedBundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) - } + event.interaction.modal( + modalObj.translateTitle(locale, resolvedBundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) + } - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) - it?.deferPublicResponseUnsafe() - } ?: return - } else { - event.interaction.deferPublicResponseUnsafe() - } + it?.deferPublicResponseUnsafe() + } ?: return + } else { + event.interaction.deferPublicResponseUnsafe() + } - val context = PublicMessageCommandContext(event, this, response, cache) + val context = PublicMessageCommandContext(event, this, response, cache) - context.populate() + context.populate() - firstSentryBreadcrumb(context) + firstSentryBreadcrumb(context) - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - emitEventAsync(PublicMessageCommandFailedChecksEvent(this, event, e.reason)) + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + emitEventAsync(PublicMessageCommandFailedChecksEvent(this, event, e.reason)) - return - } + return + } - try { - body(context, modalObj) - } catch (t: Throwable) { - emitEventAsync(PublicMessageCommandFailedWithExceptionEvent(this, event, t)) + try { + body(context, modalObj) + } catch (t: Throwable) { + emitEventAsync(PublicMessageCommandFailedWithExceptionEvent(this, event, t)) - if (t is DiscordRelayedException) { - respondText(context, t.reason, FailureReason.RelayedFailure(t)) + if (t is DiscordRelayedException) { + respondText(context, t.reason, FailureReason.RelayedFailure(t)) - return - } + return + } - handleError(context, t) + handleError(context, t) - return - } + return + } - emitEventAsync(PublicMessageCommandSucceededEvent(this, event)) - } - - override suspend fun respondText( - context: PublicMessageCommandContext, - message: String, - failureType: FailureReason<*> - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + emitEventAsync(PublicMessageCommandSucceededEvent(this, event)) + } + + override suspend fun respondText( + context: PublicMessageCommandContext, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/PublicMessageCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/PublicMessageCommandContext.kt index 0c6220db6e..766127dab8 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/PublicMessageCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/message/PublicMessageCommandContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent /** Public-only message command context. **/ public class PublicMessageCommandContext( - override val event: MessageCommandInteractionCreateEvent, - override val command: MessageCommand, M>, - override val interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap + override val event: MessageCommandInteractionCreateEvent, + override val command: MessageCommand, M>, + override val interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : MessageCommandContext, M>(event, command, cache), PublicInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/EphemeralSlashCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/EphemeralSlashCommand.kt index d43b5fd8ba..cc60d47efc 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/EphemeralSlashCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/EphemeralSlashCommand.kt @@ -26,155 +26,155 @@ import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialEphemeralSlashResponseBuilder = - (suspend InteractionResponseCreateBuilder.(ChatInputCommandInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(ChatInputCommandInteractionCreateEvent) -> Unit)? /** Ephemeral slash command. **/ public class EphemeralSlashCommand( - extension: Extension, + extension: Extension, - public override val arguments: (() -> A)? = null, - public override val modal: (() -> M)? = null, - public override val parentCommand: SlashCommand<*, *, *>? = null, - public override val parentGroup: SlashGroup? = null + public override val arguments: (() -> A)? = null, + public override val modal: (() -> M)? = null, + public override val parentCommand: SlashCommand<*, *, *>? = null, + public override val parentGroup: SlashGroup? = null, ) : SlashCommand, A, M>(extension) { - /** @suppress Internal guilder **/ - public var initialResponseBuilder: InitialEphemeralSlashResponseBuilder = null - - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialEphemeralSlashResponseBuilder) { - initialResponseBuilder = body - } - - override fun validate() { - super.validate() - - if (modal != null && initialResponseBuilder != null) { - throw InvalidCommandException( - name, - - "You may not provide a modal builder and an initial response - pick one, not both." - ) - } - } - - override suspend fun call(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { - findCommand(event).run(event, cache) - } - - override suspend fun run(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { - emitEventAsync(EphemeralSlashCommandInvocationEvent(this, event)) - - try { - if (!runChecks(event, cache)) { - emitEventAsync( - EphemeralSlashCommandFailedChecksEvent( - this, - event, - "Checks failed without a message." - ) - ) - - return - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - emitEventAsync( - EphemeralSlashCommandFailedChecksEvent( - this, - event, - e.reason - ) - ) - - return - } - - val modalObj = modal?.invoke() - - val response = if (initialResponseBuilder != null) { - event.interaction.respondEphemeral { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) - - val locale = event.getLocale() - - event.interaction.modal( - modalObj.translateTitle(locale, resolvedBundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) - } - - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) - - it?.deferEphemeralResponseUnsafe() - } ?: return - } else { - event.interaction.deferEphemeralResponseUnsafe() - } - - val context = EphemeralSlashCommandContext(event, this, response, cache) - - context.populate() - - firstSentryBreadcrumb(context, this) - - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - - emitEventAsync( - EphemeralSlashCommandFailedChecksEvent( - this, - event, - e.reason - ) - ) - - return - } - if (arguments != null) { - try { - val args = registry.argumentParser.parse(arguments, context) - - context.populateArgs(args) - } catch (e: ArgumentParsingException) { - respondText(context, e.reason, FailureReason.ArgumentParsingFailure(e)) - emitEventAsync(EphemeralSlashCommandFailedParsingEvent(this, event, e)) - - return - } - } - - try { - body(context, modalObj) - } catch (t: Throwable) { - emitEventAsync(EphemeralSlashCommandFailedWithExceptionEvent(this, event, t)) - - if (t is DiscordRelayedException) { - respondText(context, t.reason, FailureReason.RelayedFailure(t)) - - return - } - - handleError(context, t, this) - - return - } - - emitEventAsync(EphemeralSlashCommandSucceededEvent(this, event)) - } - - override suspend fun respondText( - context: EphemeralSlashCommandContext, - message: String, - failureType: FailureReason<*> - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + /** @suppress Internal guilder **/ + public var initialResponseBuilder: InitialEphemeralSlashResponseBuilder = null + + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialEphemeralSlashResponseBuilder) { + initialResponseBuilder = body + } + + override fun validate() { + super.validate() + + if (modal != null && initialResponseBuilder != null) { + throw InvalidCommandException( + name, + + "You may not provide a modal builder and an initial response - pick one, not both." + ) + } + } + + override suspend fun call(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { + findCommand(event).run(event, cache) + } + + override suspend fun run(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { + emitEventAsync(EphemeralSlashCommandInvocationEvent(this, event)) + + try { + if (!runChecks(event, cache)) { + emitEventAsync( + EphemeralSlashCommandFailedChecksEvent( + this, + event, + "Checks failed without a message." + ) + ) + + return + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + emitEventAsync( + EphemeralSlashCommandFailedChecksEvent( + this, + event, + e.reason + ) + ) + + return + } + + val modalObj = modal?.invoke() + + val response = if (initialResponseBuilder != null) { + event.interaction.respondEphemeral { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) + + val locale = event.getLocale() + + event.interaction.modal( + modalObj.translateTitle(locale, resolvedBundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) + } + + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) + + it?.deferEphemeralResponseUnsafe() + } ?: return + } else { + event.interaction.deferEphemeralResponseUnsafe() + } + + val context = EphemeralSlashCommandContext(event, this, response, cache) + + context.populate() + + firstSentryBreadcrumb(context, this) + + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + + emitEventAsync( + EphemeralSlashCommandFailedChecksEvent( + this, + event, + e.reason + ) + ) + + return + } + if (arguments != null) { + try { + val args = registry.argumentParser.parse(arguments, context) + + context.populateArgs(args) + } catch (e: ArgumentParsingException) { + respondText(context, e.reason, FailureReason.ArgumentParsingFailure(e)) + emitEventAsync(EphemeralSlashCommandFailedParsingEvent(this, event, e)) + + return + } + } + + try { + body(context, modalObj) + } catch (t: Throwable) { + emitEventAsync(EphemeralSlashCommandFailedWithExceptionEvent(this, event, t)) + + if (t is DiscordRelayedException) { + respondText(context, t.reason, FailureReason.RelayedFailure(t)) + + return + } + + handleError(context, t, this) + + return + } + + emitEventAsync(EphemeralSlashCommandSucceededEvent(this, event)) + } + + override suspend fun respondText( + context: EphemeralSlashCommandContext, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/EphemeralSlashCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/EphemeralSlashCommandContext.kt index edde2abd38..6d62567966 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/EphemeralSlashCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/EphemeralSlashCommandContext.kt @@ -15,8 +15,8 @@ import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent /** Ephemeral-only slash command context. **/ public class EphemeralSlashCommandContext( - override val event: ChatInputCommandInteractionCreateEvent, - override val command: SlashCommand, A, M>, - override val interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap + override val event: ChatInputCommandInteractionCreateEvent, + override val command: SlashCommand, A, M>, + override val interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : SlashCommandContext, A, M>(event, command, cache), EphemeralInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/PublicSlashCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/PublicSlashCommand.kt index 68b71b28ba..fe87c36a46 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/PublicSlashCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/PublicSlashCommand.kt @@ -27,143 +27,143 @@ import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialPublicSlashResponseBehavior = - (suspend InteractionResponseCreateBuilder.(ChatInputCommandInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(ChatInputCommandInteractionCreateEvent) -> Unit)? /** Public slash command. **/ public class PublicSlashCommand( - extension: Extension, + extension: Extension, - public override val arguments: (() -> A)? = null, - public override val modal: (() -> M)? = null, - public override val parentCommand: SlashCommand<*, *, *>? = null, - public override val parentGroup: SlashGroup? = null + public override val arguments: (() -> A)? = null, + public override val modal: (() -> M)? = null, + public override val parentCommand: SlashCommand<*, *, *>? = null, + public override val parentGroup: SlashGroup? = null, ) : SlashCommand, A, M>(extension) { - /** @suppress Internal builder **/ - public var initialResponseBuilder: InitialPublicSlashResponseBehavior = null + /** @suppress Internal builder **/ + public var initialResponseBuilder: InitialPublicSlashResponseBehavior = null - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialPublicSlashResponseBehavior) { - initialResponseBuilder = body - } - - override fun validate() { - super.validate() + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialPublicSlashResponseBehavior) { + initialResponseBuilder = body + } + + override fun validate() { + super.validate() - if (modal != null && initialResponseBuilder != null) { - throw InvalidCommandException( - name, + if (modal != null && initialResponseBuilder != null) { + throw InvalidCommandException( + name, - "You may not provide a modal builder and an initial response - pick one, not both." - ) - } - } + "You may not provide a modal builder and an initial response - pick one, not both." + ) + } + } - override suspend fun call(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { - findCommand(event).run(event, cache) - } + override suspend fun call(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { + findCommand(event).run(event, cache) + } - override suspend fun run(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { - emitEventAsync(PublicSlashCommandInvocationEvent(this, event)) + override suspend fun run(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { + emitEventAsync(PublicSlashCommandInvocationEvent(this, event)) - try { - if (!runChecks(event, cache)) { - emitEventAsync( - PublicSlashCommandFailedChecksEvent( - this, - event, - "Checks failed without a message." - ) - ) - - return - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - emitEventAsync(PublicSlashCommandFailedChecksEvent(this, event, e.reason)) - - return - } - - val modalObj = modal?.invoke() - - val response = if (initialResponseBuilder != null) { - event.interaction.respondPublic { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) - - val locale = event.getLocale() - - event.interaction.modal( - modalObj.translateTitle(locale, resolvedBundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) - } - - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) - - it?.deferPublicResponseUnsafe() - } ?: return - } else { - event.interaction.deferPublicResponseUnsafe() - } - - val context = PublicSlashCommandContext(event, this, response, cache) - - context.populate() - - firstSentryBreadcrumb(context, this) - - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - emitEventAsync(PublicSlashCommandFailedChecksEvent(this, event, e.reason)) - - return - } - - if (arguments != null) { - try { - val args = registry.argumentParser.parse(arguments, context) - - context.populateArgs(args) - } catch (e: ArgumentParsingException) { - respondText(context, e.reason, FailureReason.ArgumentParsingFailure(e)) - emitEventAsync(PublicSlashCommandFailedParsingEvent(this, event, e)) - - return - } - } + try { + if (!runChecks(event, cache)) { + emitEventAsync( + PublicSlashCommandFailedChecksEvent( + this, + event, + "Checks failed without a message." + ) + ) + + return + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + emitEventAsync(PublicSlashCommandFailedChecksEvent(this, event, e.reason)) + + return + } + + val modalObj = modal?.invoke() + + val response = if (initialResponseBuilder != null) { + event.interaction.respondPublic { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) + + val locale = event.getLocale() + + event.interaction.modal( + modalObj.translateTitle(locale, resolvedBundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) + } + + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) + + it?.deferPublicResponseUnsafe() + } ?: return + } else { + event.interaction.deferPublicResponseUnsafe() + } + + val context = PublicSlashCommandContext(event, this, response, cache) + + context.populate() + + firstSentryBreadcrumb(context, this) + + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + emitEventAsync(PublicSlashCommandFailedChecksEvent(this, event, e.reason)) + + return + } + + if (arguments != null) { + try { + val args = registry.argumentParser.parse(arguments, context) + + context.populateArgs(args) + } catch (e: ArgumentParsingException) { + respondText(context, e.reason, FailureReason.ArgumentParsingFailure(e)) + emitEventAsync(PublicSlashCommandFailedParsingEvent(this, event, e)) + + return + } + } - try { - body(context, modalObj) - } catch (t: Throwable) { - emitEventAsync(PublicSlashCommandFailedWithExceptionEvent(this, event, t)) + try { + body(context, modalObj) + } catch (t: Throwable) { + emitEventAsync(PublicSlashCommandFailedWithExceptionEvent(this, event, t)) - if (t is DiscordRelayedException) { - respondText(context, t.reason, FailureReason.RelayedFailure(t)) + if (t is DiscordRelayedException) { + respondText(context, t.reason, FailureReason.RelayedFailure(t)) - return - } - - handleError(context, t, this) + return + } + + handleError(context, t, this) - return - } + return + } - emitEventAsync(PublicSlashCommandSucceededEvent(this, event)) - } - - override suspend fun respondText( - context: PublicSlashCommandContext, - message: String, - failureType: FailureReason<*> - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + emitEventAsync(PublicSlashCommandSucceededEvent(this, event)) + } + + override suspend fun respondText( + context: PublicSlashCommandContext, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/PublicSlashCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/PublicSlashCommandContext.kt index e0ca672374..69e3e47d80 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/PublicSlashCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/PublicSlashCommandContext.kt @@ -15,8 +15,8 @@ import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent /** Public-only slash command context. **/ public class PublicSlashCommandContext( - override val event: ChatInputCommandInteractionCreateEvent, - override val command: SlashCommand, A, M>, - override val interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap + override val event: ChatInputCommandInteractionCreateEvent, + override val command: SlashCommand, A, M>, + override val interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : SlashCommandContext, A, M>(event, command, cache), PublicInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommand.kt index 9b1295e873..ffdec6f281 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommand.kt @@ -39,155 +39,155 @@ import org.koin.core.component.inject * @param parentGroup Parent slash command group, if any */ public abstract class SlashCommand, A : Arguments, M : ModalForm>( - extension: Extension, + extension: Extension, - public open val arguments: (() -> A)? = null, - public open val modal: (() -> M)? = null, - public open val parentCommand: SlashCommand<*, *, *>? = null, - public open val parentGroup: SlashGroup? = null, + public open val arguments: (() -> A)? = null, + public open val modal: (() -> M)? = null, + public open val parentCommand: SlashCommand<*, *, *>? = null, + public open val parentGroup: SlashGroup? = null, ) : ApplicationCommand(extension) { - /** @suppress This is only meant for use by code that extends the command system. **/ - public val kxLogger: KLogger = KotlinLogging.logger {} - - /** @suppress This is only meant for use by code that extends the command system. **/ - public val componentRegistry: ComponentRegistry by inject() - - /** Command description, as displayed on Discord. **/ - public open lateinit var description: String - - /** Command body, to be called when the command is executed. **/ - public lateinit var body: suspend C.(M?) -> Unit - - /** Whether this command has a body/action set. **/ - public open val hasBody: Boolean get() = ::body.isInitialized - - /** Map of group names to slash command groups, if any. **/ - public open val groups: MutableStringKeyedMap = mutableMapOf() - - /** List of subcommands, if any. **/ - public open val subCommands: MutableList> = mutableListOf() - - /** - * Clickable mention for this slash command, if applicable. - * - * If you're not using the [DefaultApplicationCommandRegistry] for your command registry, this will currently - * return `null`. - */ - public val mention: String? by lazy { - if (registry !is DefaultApplicationCommandRegistry) { - return@lazy null - } - - val commandRegistry = registry as DefaultApplicationCommandRegistry - - lateinit var commandId: Snowflake - - buildString { - append("") - } - } - - /** - * A [Localized] version of [description]. - */ - public val localizedDescription: Localized by lazy { localize(description) } - - override val type: ApplicationCommandType = ApplicationCommandType.ChatInput - - override var guildId: Snowflake? = if (parentCommand == null && parentGroup == null) { - settings.applicationCommandsBuilder.defaultGuild - } else { - null - } - - override fun validate() { - super.validate() - - if (!::description.isInitialized) { - throw InvalidCommandException(name, "No command description given.") - } - - if (!::body.isInitialized && groups.isEmpty() && subCommands.isEmpty()) { - throw InvalidCommandException(name, "No command action or subcommands/groups given.") - } - - val subCommandWithSubCommand = if (parentCommand != null && subCommands.isNotEmpty()) { - this - } else { - subCommands.firstOrNull { it.subCommands.isNotEmpty() } - } - - if (subCommandWithSubCommand != null) { - throw InvalidCommandException( - parentCommand?.name ?: name, - - "Subcommand ${subCommandWithSubCommand.name} has its own subcommands, but subcommands may not be " + - "nested." - ) - } - - if (::body.isInitialized && !(groups.isEmpty() && subCommands.isEmpty())) { - throw InvalidCommandException( - name, - - "Command action and subcommands/groups given, but slash commands may not have an action if they have" + - " a subcommand or group." - ) - } - - if (parentCommand != null && guildId != null) { - throw InvalidCommandException( - name, - - "Subcommands may not be limited to specific guilds - set the `guild` property on the parent command " + - "instead." - ) - } - } - - /** Call this to supply a command [body], to be called when the command is executed. **/ - public fun action(action: suspend C.(M?) -> Unit) { - body = action - } - - /** Override this to implement your command's calling logic. Check subtypes for examples! **/ - public abstract override suspend fun call( - event: ChatInputCommandInteractionCreateEvent, - cache: MutableStringKeyedMap, - ) - - /** Override this to implement a way to respond to the user, regardless of whatever happens. **/ - public abstract suspend fun respondText(context: C, message: String, failureType: FailureReason<*>) - - /** - * Override this to implement the final calling logic, including creating the command context and running with it. - */ - public abstract suspend fun run(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) - - /** If enabled, adds the initial Sentry breadcrumb to the given context. **/ - public open suspend fun firstSentryBreadcrumb(context: C, commandObj: SlashCommand<*, *, *>) { - if (sentry.enabled) { + /** @suppress This is only meant for use by code that extends the command system. **/ + public val kxLogger: KLogger = KotlinLogging.logger {} + + /** @suppress This is only meant for use by code that extends the command system. **/ + public val componentRegistry: ComponentRegistry by inject() + + /** Command description, as displayed on Discord. **/ + public open lateinit var description: String + + /** Command body, to be called when the command is executed. **/ + public lateinit var body: suspend C.(M?) -> Unit + + /** Whether this command has a body/action set. **/ + public open val hasBody: Boolean get() = ::body.isInitialized + + /** Map of group names to slash command groups, if any. **/ + public open val groups: MutableStringKeyedMap = mutableMapOf() + + /** List of subcommands, if any. **/ + public open val subCommands: MutableList> = mutableListOf() + + /** + * Clickable mention for this slash command, if applicable. + * + * If you're not using the [DefaultApplicationCommandRegistry] for your command registry, this will currently + * return `null`. + */ + public val mention: String? by lazy { + if (registry !is DefaultApplicationCommandRegistry) { + return@lazy null + } + + val commandRegistry = registry as DefaultApplicationCommandRegistry + + lateinit var commandId: Snowflake + + buildString { + append("") + } + } + + /** + * A [Localized] version of [description]. + */ + public val localizedDescription: Localized by lazy { localize(description) } + + override val type: ApplicationCommandType = ApplicationCommandType.ChatInput + + override var guildId: Snowflake? = if (parentCommand == null && parentGroup == null) { + settings.applicationCommandsBuilder.defaultGuild + } else { + null + } + + override fun validate() { + super.validate() + + if (!::description.isInitialized) { + throw InvalidCommandException(name, "No command description given.") + } + + if (!::body.isInitialized && groups.isEmpty() && subCommands.isEmpty()) { + throw InvalidCommandException(name, "No command action or subcommands/groups given.") + } + + val subCommandWithSubCommand = if (parentCommand != null && subCommands.isNotEmpty()) { + this + } else { + subCommands.firstOrNull { it.subCommands.isNotEmpty() } + } + + if (subCommandWithSubCommand != null) { + throw InvalidCommandException( + parentCommand?.name ?: name, + + "Subcommand ${subCommandWithSubCommand.name} has its own subcommands, but subcommands may not be " + + "nested." + ) + } + + if (::body.isInitialized && !(groups.isEmpty() && subCommands.isEmpty())) { + throw InvalidCommandException( + name, + + "Command action and subcommands/groups given, but slash commands may not have an action if they have" + + " a subcommand or group." + ) + } + + if (parentCommand != null && guildId != null) { + throw InvalidCommandException( + name, + + "Subcommands may not be limited to specific guilds - set the `guild` property on the parent command " + + "instead." + ) + } + } + + /** Call this to supply a command [body], to be called when the command is executed. **/ + public fun action(action: suspend C.(M?) -> Unit) { + body = action + } + + /** Override this to implement your command's calling logic. Check subtypes for examples! **/ + public abstract override suspend fun call( + event: ChatInputCommandInteractionCreateEvent, + cache: MutableStringKeyedMap, + ) + + /** Override this to implement a way to respond to the user, regardless of whatever happens. **/ + public abstract suspend fun respondText(context: C, message: String, failureType: FailureReason<*>) + + /** + * Override this to implement the final calling logic, including creating the command context and running with it. + */ + public abstract suspend fun run(event: ChatInputCommandInteractionCreateEvent, cache: MutableStringKeyedMap) + + /** If enabled, adds the initial Sentry breadcrumb to the given context. **/ + public open suspend fun firstSentryBreadcrumb(context: C, commandObj: SlashCommand<*, *, *>) { + if (sentry.enabled) { context.sentry.context( "command", @@ -201,104 +201,104 @@ public abstract class SlashCommand, A : Argumen "extension", extension.name ) - context.sentry.breadcrumb(BreadcrumbType.User) { - category = "command.application.slash" - message = "Slash command \"${commandObj.name}\" called." + context.sentry.breadcrumb(BreadcrumbType.User) { + category = "command.application.slash" + message = "Slash command \"${commandObj.name}\" called." - channel = context.channel.asChannelOrNull() - guild = context.guild?.asGuildOrNull() + channel = context.channel.asChannelOrNull() + guild = context.guild?.asGuildOrNull() - data["command"] = commandObj.name - } - } - } + data["command"] = commandObj.name + } + } + } - override suspend fun runChecks( - event: ChatInputCommandInteractionCreateEvent, - cache: MutableStringKeyedMap, - ): Boolean { - val locale = event.getLocale() - var result = super.runChecks(event, cache) + override suspend fun runChecks( + event: ChatInputCommandInteractionCreateEvent, + cache: MutableStringKeyedMap, + ): Boolean { + val locale = event.getLocale() + var result = super.runChecks(event, cache) - if (result && parentCommand != null) { - result = parentCommand!!.runChecks(event, cache) - } + if (result && parentCommand != null) { + result = parentCommand!!.runChecks(event, cache) + } - if (result && parentGroup != null) { - result = parentGroup!!.parent.runChecks(event, cache) - } + if (result && parentGroup != null) { + result = parentGroup!!.parent.runChecks(event, cache) + } - if (result) { - settings.applicationCommandsBuilder.slashCommandChecks.forEach { check -> - val context = CheckContextWithCache(event, locale, cache) + if (result) { + settings.applicationCommandsBuilder.slashCommandChecks.forEach { check -> + val context = CheckContextWithCache(event, locale, cache) - check(context) + check(context) - if (!context.passed) { - context.throwIfFailedWithMessage() + if (!context.passed) { + context.throwIfFailedWithMessage() - return false - } - } + return false + } + } - extension.slashCommandChecks.forEach { check -> - val context = CheckContextWithCache(event, locale, cache) + extension.slashCommandChecks.forEach { check -> + val context = CheckContextWithCache(event, locale, cache) - check(context) + check(context) - if (!context.passed) { - context.throwIfFailedWithMessage() + if (!context.passed) { + context.throwIfFailedWithMessage() - return false - } - } - } + return false + } + } + } - return result - } + return result + } - /** Given a command event, resolve the correct command or subcommand object. **/ - public open fun findCommand(event: ChatInputCommandInteractionCreateEvent): SlashCommand<*, *, *> = - findCommand(event.interaction.command) + /** Given a command event, resolve the correct command or subcommand object. **/ + public open fun findCommand(event: ChatInputCommandInteractionCreateEvent): SlashCommand<*, *, *> = + findCommand(event.interaction.command) - /** Given an autocomplete event, resolve the correct command or subcommand object. **/ - public open fun findCommand(event: AutoCompleteInteractionCreateEvent): SlashCommand<*, *, *> = - findCommand(event.interaction.command) + /** Given an autocomplete event, resolve the correct command or subcommand object. **/ + public open fun findCommand(event: AutoCompleteInteractionCreateEvent): SlashCommand<*, *, *> = + findCommand(event.interaction.command) - /** Given an [InteractionCommand], resolve the correct command or subcommand object. **/ - public open fun findCommand(eventCommand: InteractionCommand): SlashCommand<*, *, *> = - when (eventCommand) { - is SubCommand -> { - val firstSubCommandKey = eventCommand.name + /** Given an [InteractionCommand], resolve the correct command or subcommand object. **/ + public open fun findCommand(eventCommand: InteractionCommand): SlashCommand<*, *, *> = + when (eventCommand) { + is SubCommand -> { + val firstSubCommandKey = eventCommand.name - this.subCommands.firstOrNull { it.localizedName.default == firstSubCommandKey } - ?: error("Unknown subcommand: $firstSubCommandKey") - } + this.subCommands.firstOrNull { it.localizedName.default == firstSubCommandKey } + ?: error("Unknown subcommand: $firstSubCommandKey") + } - is GroupCommand -> { - val firstEventGroupKey = eventCommand.groupName - val group = this.groups[firstEventGroupKey] ?: error("Unknown command group: $firstEventGroupKey") - val firstSubCommandKey = eventCommand.name + is GroupCommand -> { + val firstEventGroupKey = eventCommand.groupName + val group = this.groups[firstEventGroupKey] ?: error("Unknown command group: $firstEventGroupKey") + val firstSubCommandKey = eventCommand.name - group.subCommands.firstOrNull { it.localizedName.default == firstSubCommandKey } - ?: error("Unknown subcommand: $firstSubCommandKey") - } + group.subCommands.firstOrNull { it.localizedName.default == firstSubCommandKey } + ?: error("Unknown subcommand: $firstSubCommandKey") + } - else -> this - } + else -> this + } - /** A general way to handle errors thrown during the course of a command's execution. **/ + /** A general way to handle errors thrown during the course of a command's execution. **/ @Suppress("StringLiteralDuplication") public open suspend fun handleError(context: C, t: Throwable, commandObj: SlashCommand<*, *, *>) { - kxLogger.error(t) { "Error during execution of ${commandObj.name} slash command (${context.event})" } + kxLogger.error(t) { "Error during execution of ${commandObj.name} slash command (${context.event})" } - if (sentry.enabled) { - kxLogger.trace { "Submitting error to sentry." } + if (sentry.enabled) { + kxLogger.trace { "Submitting error to sentry." } - val sentryId = context.sentry.captureThrowable(t) { + val sentryId = context.sentry.captureThrowable(t) { channel = context.channel.asChannelOrNull() user = context.user.asUserOrNull() - } + } val errorMessage = if (sentryId != null) { kxLogger.info { "Error submitted to Sentry: $sentryId" } @@ -312,13 +312,13 @@ public abstract class SlashCommand, A : Argumen context.translate("commands.error.user", null) } - respondText(context, errorMessage, FailureReason.ExecutionError(t)) - } else { - respondText( - context, - context.translate("commands.error.user", null), - FailureReason.ExecutionError(t) - ) - } - } + respondText(context, errorMessage, FailureReason.ExecutionError(t)) + } else { + respondText( + context, + context.translate("commands.error.user", null), + FailureReason.ExecutionError(t) + ) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommandContext.kt index 8e7ba17154..3276636dda 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommandContext.kt @@ -18,15 +18,15 @@ import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent * @param event Event that triggered this slash command invocation. */ public open class SlashCommandContext, A : Arguments, M : ModalForm>( - public open val event: ChatInputCommandInteractionCreateEvent, - public override val command: SlashCommand, - cache: MutableStringKeyedMap + public open val event: ChatInputCommandInteractionCreateEvent, + public override val command: SlashCommand, + cache: MutableStringKeyedMap, ) : ApplicationCommandContext(event, command, cache) { - /** Object representing this slash command's arguments, if any. **/ - public open lateinit var arguments: A + /** Object representing this slash command's arguments, if any. **/ + public open lateinit var arguments: A - /** @suppress Internal function for copying args object in later. **/ - public fun populateArgs(args: A) { - arguments = args - } + /** @suppress Internal function for copying args object in later. **/ + public fun populateArgs(args: A) { + arguments = args + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommandParser.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommandParser.kt index ca05088ebf..31982ca78f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommandParser.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashCommandParser.kt @@ -5,7 +5,7 @@ */ @file:Suppress( - "StringLiteralDuplication" // Needs cleaning up with polymorphism later anyway + "StringLiteralDuplication" // Needs cleaning up with polymorphism later anyway ) package com.kotlindiscord.kord.extensions.commands.application.slash @@ -29,331 +29,331 @@ private val logger = KotlinLogging.logger {} * Discord's API. Coalescing converters will act like single converters. */ public open class SlashCommandParser { - /** - * Parse the arguments for this slash command, which have been provided by Discord. - * - * Instead of taking the objects as Discord provides them, this function will stringify all the command's - * arguments. This allows them to be passed through the usual converter system. - */ - public suspend fun parse( - builder: () -> T, - context: SlashCommandContext<*, *, *>, - ): T { - val argumentsObj = builder.invoke() - argumentsObj.validate() - - logger.trace { "Arguments object: $argumentsObj (${argumentsObj.args.size} args)" } - - val args = argumentsObj.args.toMutableList() - val command = context.event.interaction.command - - val values = command.options.mapValues { - if (it.value is StringOptionValue) { - StringOptionValue((it.value.value as String).trim(), it.value.focused) - } else { - it.value - } - } as Map> - - var currentArg: Argument<*>? - var currentValue: OptionValue<*>? - - @Suppress("LoopWithTooManyJumpStatements") // Listen here u lil shit - while (true) { - currentArg = args.removeFirstOrNull() - currentArg ?: break // If it's null, we're out of arguments - - logger.trace { "Current argument: ${currentArg.displayName}" } - - currentValue = - values[currentArg.getDefaultTranslatedDisplayName(context.translationsProvider, context.command)] - - logger.trace { "Current value: $currentValue" } - - @Suppress("TooGenericExceptionCaught") - when (val converter = currentArg.converter) { - // It's worth noting that Discord handles validation for required converters, so we don't need to - // do that checking ourselves, really - - is SingleConverter<*> -> try { - val parsed = if (currentValue != null) { - converter.parseOption(context, currentValue) - } else { - false - } - - if (converter.required && !parsed) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.invalidValue", - - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), - - converter.getErrorString(context), - currentValue - ) - ), - - "argumentParser.error.invalidValue", + /** + * Parse the arguments for this slash command, which have been provided by Discord. + * + * Instead of taking the objects as Discord provides them, this function will stringify all the command's + * arguments. This allows them to be passed through the usual converter system. + */ + public suspend fun parse( + builder: () -> T, + context: SlashCommandContext<*, *, *>, + ): T { + val argumentsObj = builder.invoke() + argumentsObj.validate() + + logger.trace { "Arguments object: $argumentsObj (${argumentsObj.args.size} args)" } + + val args = argumentsObj.args.toMutableList() + val command = context.event.interaction.command + + val values = command.options.mapValues { + if (it.value is StringOptionValue) { + StringOptionValue((it.value.value as String).trim(), it.value.focused) + } else { + it.value + } + } as Map> + + var currentArg: Argument<*>? + var currentValue: OptionValue<*>? + + @Suppress("LoopWithTooManyJumpStatements") // Listen here u lil shit + while (true) { + currentArg = args.removeFirstOrNull() + currentArg ?: break // If it's null, we're out of arguments + + logger.trace { "Current argument: ${currentArg.displayName}" } + + currentValue = + values[currentArg.getDefaultTranslatedDisplayName(context.translationsProvider, context.command)] + + logger.trace { "Current value: $currentValue" } + + @Suppress("TooGenericExceptionCaught") + when (val converter = currentArg.converter) { + // It's worth noting that Discord handles validation for required converters, so we don't need to + // do that checking ourselves, really + + is SingleConverter<*> -> try { + val parsed = if (currentValue != null) { + converter.parseOption(context, currentValue) + } else { + false + } + + if (converter.required && !parsed) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.invalidValue", + + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), + + converter.getErrorString(context), + currentValue + ) + ), + + "argumentParser.error.invalidValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - null - ) - } + currentArg, + argumentsObj, + null + ) + } - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } - converter.parseSuccess = true - currentValue = null + converter.parseSuccess = true + currentValue = null - converter.validate(context) - } - } catch (e: DiscordRelayedException) { - if (converter.required) { - throw ArgumentParsingException( - converter.handleError(e, context), - null, + converter.validate(context) + } + } catch (e: DiscordRelayedException) { + if (converter.required) { + throw ArgumentParsingException( + converter.handleError(e, context), + null, context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - null - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required) { - throw t - } - } - - is CoalescingConverter<*> -> try { - val parsed = if (currentValue != null) { - converter.parseOption(context, currentValue) - } else { - false - } - - if (converter.required && !parsed) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.invalidValue", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), - - converter.getErrorString(context), - currentValue - ) - ), - - "argumentParser.error.invalidValue", + currentArg, + argumentsObj, + null + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required) { + throw t + } + } + + is CoalescingConverter<*> -> try { + val parsed = if (currentValue != null) { + converter.parseOption(context, currentValue) + } else { + false + } + + if (converter.required && !parsed) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.invalidValue", + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), + + converter.getErrorString(context), + currentValue + ) + ), + + "argumentParser.error.invalidValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - null - ) - } + currentArg, + argumentsObj, + null + ) + } - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } - converter.parseSuccess = true - currentValue = null + converter.parseSuccess = true + currentValue = null - converter.validate(context) - } - } catch (e: DiscordRelayedException) { - if (converter.required) { - throw ArgumentParsingException( - converter.handleError(e, context), - null, + converter.validate(context) + } + } catch (e: DiscordRelayedException) { + if (converter.required) { + throw ArgumentParsingException( + converter.handleError(e, context), + null, context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - null - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required) { - throw t - } - } - - is OptionalConverter<*> -> try { - val parsed = if (currentValue != null) { - converter.parseOption(context, currentValue) - } else { - false - } - - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } - - converter.parseSuccess = true - currentValue = null - } - - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required || converter.outputError) { - throw ArgumentParsingException( - converter.handleError(e, context), - null, + currentArg, + argumentsObj, + null + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required) { + throw t + } + } + + is OptionalConverter<*> -> try { + val parsed = if (currentValue != null) { + converter.parseOption(context, currentValue) + } else { + false + } + + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } + + converter.parseSuccess = true + currentValue = null + } + + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required || converter.outputError) { + throw ArgumentParsingException( + converter.handleError(e, context), + null, context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - null - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required) { - throw t - } - } - - is OptionalCoalescingConverter<*> -> try { - val parsed = if (currentValue != null) { - converter.parseOption(context, currentValue) - } else { - false - } - - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } - - converter.parseSuccess = true - currentValue = null - } - - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required || converter.outputError) { - throw ArgumentParsingException( - converter.handleError(e, context), - null, + currentArg, + argumentsObj, + null + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required) { + throw t + } + } + + is OptionalCoalescingConverter<*> -> try { + val parsed = if (currentValue != null) { + converter.parseOption(context, currentValue) + } else { + false + } + + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } + + converter.parseSuccess = true + currentValue = null + } + + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required || converter.outputError) { + throw ArgumentParsingException( + converter.handleError(e, context), + null, context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - null - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required) { - throw t - } - } - - is DefaultingConverter<*> -> try { - val parsed = if (currentValue != null) { - converter.parseOption(context, currentValue) - } else { - false - } - - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } - - converter.parseSuccess = true - currentValue = null - } - - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required || converter.outputError) { - throw ArgumentParsingException( - converter.handleError(e, context), - null, + currentArg, + argumentsObj, + null + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required) { + throw t + } + } + + is DefaultingConverter<*> -> try { + val parsed = if (currentValue != null) { + converter.parseOption(context, currentValue) + } else { + false + } + + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } + + converter.parseSuccess = true + currentValue = null + } + + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required || converter.outputError) { + throw ArgumentParsingException( + converter.handleError(e, context), + null, context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - null - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required) { - throw t - } - } - - is DefaultingCoalescingConverter<*> -> try { - val parsed = if (currentValue != null) { - converter.parseOption(context, currentValue) - } else { - false - } - - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } - - converter.parseSuccess = true - currentValue = null - } - - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required || converter.outputError) { - throw ArgumentParsingException( - converter.handleError(e, context), - null, + currentArg, + argumentsObj, + null + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required) { + throw t + } + } + + is DefaultingCoalescingConverter<*> -> try { + val parsed = if (currentValue != null) { + converter.parseOption(context, currentValue) + } else { + false + } + + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } + + converter.parseSuccess = true + currentValue = null + } + + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required || converter.outputError) { + throw ArgumentParsingException( + converter.handleError(e, context), + null, context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - null - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required) { - throw t - } - } - - else -> error("Unsupported type for converter: $converter") - } - } - - return argumentsObj - } + currentArg, + argumentsObj, + null + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required) { + throw t + } + } + + else -> error("Unsupported type for converter: $converter") + } + } + + return argumentsObj + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashGroup.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashGroup.kt index b63c029759..150159991e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashGroup.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/SlashGroup.kt @@ -22,61 +22,61 @@ import java.util.* * @param parent Parent slash command that this group belongs to */ public class SlashGroup( - public val name: String, - public val parent: SlashCommand<*, *, *> + public val name: String, + public val parent: SlashCommand<*, *, *>, ) : KordExKoinComponent { - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by inject() + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by inject() - /** @suppress **/ - public val logger: KLogger = KotlinLogging.logger {} + /** @suppress **/ + public val logger: KLogger = KotlinLogging.logger {} - /** List of subcommands belonging to this group. **/ - public val subCommands: MutableList> = mutableListOf() + /** List of subcommands belonging to this group. **/ + public val subCommands: MutableList> = mutableListOf() - /** Command group description, which is required and shown on Discord. **/ - public lateinit var description: String + /** Command group description, which is required and shown on Discord. **/ + public lateinit var description: String - /** - * A [Localized] version of [name]. - */ - public val localizedName: Localized by lazy { parent.localize(name, true) } + /** + * A [Localized] version of [name]. + */ + public val localizedName: Localized by lazy { parent.localize(name, true) } - /** - * A [Localized] version of [description]. - */ - public val localizedDescription: Localized by lazy { parent.localize(description) } + /** + * A [Localized] version of [description]. + */ + public val localizedDescription: Localized by lazy { parent.localize(description) } - /** Translation cache, so we don't have to look up translations every time. **/ - public val descriptionTranslationCache: MutableMap = mutableMapOf() + /** Translation cache, so we don't have to look up translations every time. **/ + public val descriptionTranslationCache: MutableMap = mutableMapOf() - /** Return this group's description translated for the given locale, cached as required. **/ - public fun getTranslatedDescription(locale: Locale): String { - // Only slash commands need this to be lower-cased. + /** Return this group's description translated for the given locale, cached as required. **/ + public fun getTranslatedDescription(locale: Locale): String { + // Only slash commands need this to be lower-cased. - if (!descriptionTranslationCache.containsKey(locale)) { - descriptionTranslationCache[locale] = translationsProvider.translate( - this.description, - this.parent.resolvedBundle, - locale - ).lowercase() - } + if (!descriptionTranslationCache.containsKey(locale)) { + descriptionTranslationCache[locale] = translationsProvider.translate( + this.description, + this.parent.resolvedBundle, + locale + ).lowercase() + } - return descriptionTranslationCache[locale]!! - } + return descriptionTranslationCache[locale]!! + } - /** - * Validate this command group, ensuring it has everything it needs. - * - * Throws if not. - */ - public fun validate() { - if (!::description.isInitialized) { - throw InvalidCommandException(name, "No group description given.") - } + /** + * Validate this command group, ensuring it has everything it needs. + * + * Throws if not. + */ + public fun validate() { + if (!::description.isInitialized) { + throw InvalidCommandException(name, "No group description given.") + } - if (subCommands.isEmpty()) { - error("Command groups must contain at least one subcommand.") - } - } + if (subCommands.isEmpty()) { + error("Command groups must contain at least one subcommand.") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/_Functions.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/_Functions.kt index 236b053db5..f4ecac18ff 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/_Functions.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/_Functions.kt @@ -28,28 +28,28 @@ private const val SUBCOMMAND_AND_GROUP_LIMIT: Int = 25 * @param body Lambda used to build the [SlashGroup] object. */ public suspend fun SlashCommand<*, *, *>.group(name: String, body: suspend SlashGroup.() -> Unit): SlashGroup { - if (parentCommand != null) { - error("Command groups may not be nested inside subcommands.") - } + if (parentCommand != null) { + error("Command groups may not be nested inside subcommands.") + } - if (groups.size >= SUBCOMMAND_AND_GROUP_LIMIT) { - error("Commands may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT command groups.") - } + if (groups.size >= SUBCOMMAND_AND_GROUP_LIMIT) { + error("Commands may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT command groups.") + } - val localizedGroupName = localize(name, true).default + val localizedGroupName = localize(name, true).default - if (groups[localizedGroupName] != null) { - error("A command group with the name '$name' has already been registered.") - } + if (groups[localizedGroupName] != null) { + error("A command group with the name '$name' has already been registered.") + } - val group = SlashGroup(name, this) + val group = SlashGroup(name, this) - body(group) - group.validate() + body(group) + group.validate() - groups[localizedGroupName] = group + groups[localizedGroupName] = group - return group + return group } // endregion @@ -65,20 +65,20 @@ public suspend fun SlashCommand<*, *, *>.group(name: String, body: suspend Slash * @param body Builder lambda used for setting up the slash command object. */ public suspend fun SlashCommand<*, *, *>.ephemeralSubCommand( - arguments: () -> T, - body: suspend EphemeralSlashCommand.() -> Unit, + arguments: () -> T, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - extension, - arguments, - null, - this, - parentGroup - ) + val commandObj = EphemeralSlashCommand( + extension, + arguments, + null, + this, + parentGroup + ) - body(commandObj) + body(commandObj) - return ephemeralSubCommand(commandObj) + return ephemeralSubCommand(commandObj) } /** @@ -92,20 +92,20 @@ public suspend fun SlashCommand<*, *, *>.ephemeralSubCommand( @ExtensionDSL @JvmName("ephemeralSubCommand1") public suspend fun SlashCommand<*, *, *>.ephemeralSubCommand( - modal: () -> M, - body: suspend EphemeralSlashCommand.() -> Unit + modal: () -> M, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - extension, - null, - modal, - this, - null - ) + val commandObj = EphemeralSlashCommand( + extension, + null, + modal, + this, + null + ) - body(commandObj) + body(commandObj) - return ephemeralSubCommand(commandObj) + return ephemeralSubCommand(commandObj) } /** @@ -119,21 +119,21 @@ public suspend fun SlashCommand<*, *, *>.ephemeralSubCommand( */ @ExtensionDSL public suspend fun SlashCommand<*, *, *>.ephemeralSubCommand( - arguments: () -> A, - modal: () -> M, - body: suspend EphemeralSlashCommand.() -> Unit + arguments: () -> A, + modal: () -> M, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - extension, - arguments, - modal, - this, - null - ) + val commandObj = EphemeralSlashCommand( + extension, + arguments, + modal, + this, + null + ) - body(commandObj) + body(commandObj) - return ephemeralSubCommand(commandObj) + return ephemeralSubCommand(commandObj) } /** @@ -144,27 +144,27 @@ public suspend fun SlashCommand<*, *, *>.ephemera * @param commandObj EphemeralSlashCommand object to register as a subcommand. */ public fun SlashCommand<*, *, *>.ephemeralSubCommand( - commandObj: EphemeralSlashCommand, + commandObj: EphemeralSlashCommand, ): EphemeralSlashCommand { - commandObj.guildId = null - - if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { - throw InvalidCommandException( - commandObj.name, - "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." - ) - } - - try { - commandObj.validate() - subCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - kxLogger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - kxLogger.error(e) { "Failed to register subcommand - $e" } - } - - return commandObj + commandObj.guildId = null + + if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { + throw InvalidCommandException( + commandObj.name, + "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." + ) + } + + try { + commandObj.validate() + subCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + kxLogger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + kxLogger.error(e) { "Failed to register subcommand - $e" } + } + + return commandObj } /** @@ -175,19 +175,19 @@ public fun SlashCommand<*, *, *>.ephemeralSubComm * @param body Builder lambda used for setting up the subcommand object. */ public suspend fun SlashCommand<*, *, *>.ephemeralSubCommand( - body: suspend EphemeralSlashCommand.() -> Unit, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - extension, - null, - null, - this, - parentGroup - ) + val commandObj = EphemeralSlashCommand( + extension, + null, + null, + this, + parentGroup + ) - body(commandObj) + body(commandObj) - return ephemeralSubCommand(commandObj) + return ephemeralSubCommand(commandObj) } // endregion @@ -203,20 +203,20 @@ public suspend fun SlashCommand<*, *, *>.ephemeralSubCommand( * @param body Builder lambda used for setting up the slash command object. */ public suspend fun SlashCommand<*, *, *>.publicSubCommand( - arguments: () -> T, - body: suspend PublicSlashCommand.() -> Unit, + arguments: () -> T, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - extension, - arguments, - null, - this, - parentGroup - ) + val commandObj = PublicSlashCommand( + extension, + arguments, + null, + this, + parentGroup + ) - body(commandObj) + body(commandObj) - return publicSubCommand(commandObj) + return publicSubCommand(commandObj) } /** @@ -230,20 +230,20 @@ public suspend fun SlashCommand<*, *, *>.publicSubCommand( @ExtensionDSL @JvmName("publicSubCommand1") public suspend fun SlashCommand<*, *, *>.publicSubCommand( - modal: () -> M, - body: suspend PublicSlashCommand.() -> Unit + modal: () -> M, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - extension, - null, - modal, - this, - null - ) + val commandObj = PublicSlashCommand( + extension, + null, + modal, + this, + null + ) - body(commandObj) + body(commandObj) - return publicSubCommand(commandObj) + return publicSubCommand(commandObj) } /** @@ -257,20 +257,20 @@ public suspend fun SlashCommand<*, *, *>.publicSubCommand( */ @ExtensionDSL public suspend fun SlashCommand<*, *, *>.publicSubCommand( - arguments: () -> A, - modal: () -> M, - body: suspend PublicSlashCommand.() -> Unit + arguments: () -> A, + modal: () -> M, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - extension, - arguments, - modal, - this - ) + val commandObj = PublicSlashCommand( + extension, + arguments, + modal, + this + ) - body(commandObj) + body(commandObj) - return publicSubCommand(commandObj) + return publicSubCommand(commandObj) } /** @@ -281,27 +281,27 @@ public suspend fun SlashCommand<*, *, *>.publicSu * @param commandObj PublicSlashCommand object to register as a subcommand. */ public fun SlashCommand<*, *, *>.publicSubCommand( - commandObj: PublicSlashCommand, + commandObj: PublicSlashCommand, ): PublicSlashCommand { - commandObj.guildId = null - - if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { - throw InvalidCommandException( - commandObj.name, - "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." - ) - } - - try { - commandObj.validate() - subCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - kxLogger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - kxLogger.error(e) { "Failed to register subcommand - $e" } - } - - return commandObj + commandObj.guildId = null + + if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { + throw InvalidCommandException( + commandObj.name, + "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." + ) + } + + try { + commandObj.validate() + subCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + kxLogger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + kxLogger.error(e) { "Failed to register subcommand - $e" } + } + + return commandObj } /** @@ -312,19 +312,19 @@ public fun SlashCommand<*, *, *>.publicSubCommand * @param body Builder lambda used for setting up the subcommand object. */ public suspend fun SlashCommand<*, *, *>.publicSubCommand( - body: suspend PublicSlashCommand.() -> Unit, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - extension, - null, - null, - this, - parentGroup - ) + val commandObj = PublicSlashCommand( + extension, + null, + null, + this, + parentGroup + ) - body(commandObj) + body(commandObj) - return publicSubCommand(commandObj) + return publicSubCommand(commandObj) } // endregion @@ -340,20 +340,20 @@ public suspend fun SlashCommand<*, *, *>.publicSubCommand( * @param body Builder lambda used for setting up the slash command object. */ public suspend fun SlashGroup.ephemeralSubCommand( - arguments: () -> T, - body: suspend EphemeralSlashCommand.() -> Unit, + arguments: () -> T, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - parent.extension, - arguments, - null, - parent, - this - ) + val commandObj = EphemeralSlashCommand( + parent.extension, + arguments, + null, + parent, + this + ) - body(commandObj) + body(commandObj) - return ephemeralSubCommand(commandObj) + return ephemeralSubCommand(commandObj) } /** @@ -364,27 +364,27 @@ public suspend fun SlashGroup.ephemeralSubCommand( * @param commandObj EphemeralSlashCommand object to register as a subcommand. */ public fun SlashGroup.ephemeralSubCommand( - commandObj: EphemeralSlashCommand, + commandObj: EphemeralSlashCommand, ): EphemeralSlashCommand { - commandObj.guildId = null - - if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { - throw InvalidCommandException( - commandObj.name, - "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." - ) - } - - try { - commandObj.validate() - subCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register subcommand - $e" } - } - - return commandObj + commandObj.guildId = null + + if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { + throw InvalidCommandException( + commandObj.name, + "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." + ) + } + + try { + commandObj.validate() + subCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register subcommand - $e" } + } + + return commandObj } /** @@ -395,19 +395,19 @@ public fun SlashGroup.ephemeralSubCommand( * @param body Builder lambda used for setting up the subcommand object. */ public suspend fun SlashGroup.ephemeralSubCommand( - body: suspend EphemeralSlashCommand.() -> Unit, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - parent.extension, - null, - null, - parent, - this - ) + val commandObj = EphemeralSlashCommand( + parent.extension, + null, + null, + parent, + this + ) - body(commandObj) + body(commandObj) - return ephemeralSubCommand(commandObj) + return ephemeralSubCommand(commandObj) } // endregion @@ -423,20 +423,20 @@ public suspend fun SlashGroup.ephemeralSubCommand( * @param body Builder lambda used for setting up the slash command object. */ public suspend fun SlashGroup.publicSubCommand( - arguments: () -> T, - body: suspend PublicSlashCommand.() -> Unit, + arguments: () -> T, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - parent.extension, - arguments, - null, - parent, - this - ) + val commandObj = PublicSlashCommand( + parent.extension, + arguments, + null, + parent, + this + ) - body(commandObj) + body(commandObj) - return publicSubCommand(commandObj) + return publicSubCommand(commandObj) } /** @@ -447,27 +447,27 @@ public suspend fun SlashGroup.publicSubCommand( * @param commandObj PublicSlashCommand object to register as a subcommand. */ public fun SlashGroup.publicSubCommand( - commandObj: PublicSlashCommand, + commandObj: PublicSlashCommand, ): PublicSlashCommand { - commandObj.guildId = null - - if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { - throw InvalidCommandException( - commandObj.name, - "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." - ) - } - - try { - commandObj.validate() - subCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register subcommand - $e" } - } - - return commandObj + commandObj.guildId = null + + if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { + throw InvalidCommandException( + commandObj.name, + "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." + ) + } + + try { + commandObj.validate() + subCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register subcommand - $e" } + } + + return commandObj } /** @@ -478,19 +478,19 @@ public fun SlashGroup.publicSubCommand( * @param body Builder lambda used for setting up the subcommand object. */ public suspend fun SlashGroup.publicSubCommand( - body: suspend PublicSlashCommand.() -> Unit, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - parent.extension, - null, - null, - parent, - this - ) + val commandObj = PublicSlashCommand( + parent.extension, + null, + null, + parent, + this + ) - body(commandObj) + body(commandObj) - return publicSubCommand(commandObj) + return publicSubCommand(commandObj) } // endregion diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/ChoiceConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/ChoiceConverter.kt index c56e9da65e..a0e995e725 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/ChoiceConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/ChoiceConverter.kt @@ -17,11 +17,11 @@ private const val CHOICE_LIMIT = 25 // Discord doesn't allow more choices than */ public abstract class ChoiceConverter( - public open val choices: Map + public open val choices: Map, ) : SingleConverter() { - init { - if (choices.size > CHOICE_LIMIT) { - error("You may not provide more than $CHOICE_LIMIT choices.") - } - } + init { + if (choices.size > CHOICE_LIMIT) { + error("You may not provide more than $CHOICE_LIMIT choices.") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/ChoiceEnum.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/ChoiceEnum.kt index 8829507e47..21c7fe7fbc 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/ChoiceEnum.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/ChoiceEnum.kt @@ -10,6 +10,6 @@ import com.kotlindiscord.kord.extensions.commands.application.slash.converters.i /** Interface representing an enum used in the [EnumChoiceConverter]. **/ public interface ChoiceEnum { - /** Human-readable name to show on Discord. **/ - public val readableName: String + /** Human-readable name to show on Discord. **/ + public val readableName: String } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/EnumChoiceConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/EnumChoiceConverter.kt index 98c6665d98..7997c7879e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/EnumChoiceConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/EnumChoiceConverter.kt @@ -25,85 +25,85 @@ import dev.kord.rest.builder.interaction.StringChoiceBuilder * All enums used for this must implement the [ChoiceEnum] interface. */ @Converter( - "enum", - - types = [ConverterType.SINGLE, ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.CHOICE], - imports = [ - "com.kotlindiscord.kord.extensions.commands.converters.impl.getEnum", - "com.kotlindiscord.kord.extensions.commands.application.slash.converters.ChoiceEnum" - ], - - builderGeneric = "E", - builderConstructorArguments = [ - "public var getter: suspend (String) -> E?", - "!! argMap: Map", - ], - - builderFields = [ - "public lateinit var typeName: String", - "public var bundle: String? = null" - ], - - builderInitStatements = [ - "choices(argMap)" - ], - - builderSuffixedWhere = "E : Enum, E : ChoiceEnum", - - functionGeneric = "E", - functionBuilderArguments = [ - "getter = { getEnum(it) }", - "argMap = enumValues().associateBy { it.readableName }", - ], - - functionSuffixedWhere = "E : Enum, E : ChoiceEnum" + "enum", + + types = [ConverterType.SINGLE, ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.CHOICE], + imports = [ + "com.kotlindiscord.kord.extensions.commands.converters.impl.getEnum", + "com.kotlindiscord.kord.extensions.commands.application.slash.converters.ChoiceEnum" + ], + + builderGeneric = "E", + builderConstructorArguments = [ + "public var getter: suspend (String) -> E?", + "!! argMap: Map", + ], + + builderFields = [ + "public lateinit var typeName: String", + "public var bundle: String? = null" + ], + + builderInitStatements = [ + "choices(argMap)" + ], + + builderSuffixedWhere = "E : Enum, E : ChoiceEnum", + + functionGeneric = "E", + functionBuilderArguments = [ + "getter = { getEnum(it) }", + "argMap = enumValues().associateBy { it.readableName }", + ], + + functionSuffixedWhere = "E : Enum, E : ChoiceEnum" ) public class EnumChoiceConverter( - typeName: String, - private val getter: suspend (String) -> E?, - choices: Map, - override var validator: Validator = null, - override val bundle: String? = null, + typeName: String, + private val getter: suspend (String) -> E?, + choices: Map, + override var validator: Validator = null, + override val bundle: String? = null, ) : ChoiceConverter(choices) where E : Enum, E : ChoiceEnum { - override val signatureTypeString: String = typeName + override val signatureTypeString: String = typeName - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - try { - parsed = getter.invoke(arg) ?: return false - } catch (e: IllegalArgumentException) { - return false - } + try { + parsed = getter.invoke(arg) ?: return false + } catch (e: IllegalArgumentException) { + return false + } - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { - required = true + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { + required = true - this@EnumChoiceConverter.choices.forEach { choice(it.key, it.value.name) } - } + this@EnumChoiceConverter.choices.forEach { choice(it.key, it.value.name) } + } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val stringOption = option as? StringOptionValue ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val stringOption = option as? StringOptionValue ?: return false - try { - parsed = getter.invoke(stringOption.value) ?: return false - } catch (e: IllegalArgumentException) { - return false - } + try { + parsed = getter.invoke(stringOption.value) ?: return false + } catch (e: IllegalArgumentException) { + return false + } - return true - } + return true + } } /** * The default choice enum value getter - matches choice enums via a case-insensitive string comparison with the names. */ public inline fun getEnum(arg: String): E? where E : Enum, E : ChoiceEnum = - enumValues().firstOrNull { - it.readableName.equals(arg, true) || - it.name.equals(arg, true) - } + enumValues().firstOrNull { + it.readableName.equals(arg, true) || + it.name.equals(arg, true) + } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/NumberChoiceConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/NumberChoiceConverter.kt index 03a7d37893..a6942544b6 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/NumberChoiceConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/NumberChoiceConverter.kt @@ -27,47 +27,47 @@ private const val DEFAULT_RADIX = 10 * Discord doesn't support longs or floating point types, so this is the only numeric type you can use directly. */ @Converter( - "number", + "number", - types = [ConverterType.CHOICE, ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE], - builderFields = ["public var radix: Int = $DEFAULT_RADIX"] + types = [ConverterType.CHOICE, ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE], + builderFields = ["public var radix: Int = $DEFAULT_RADIX"] ) public class NumberChoiceConverter( - private val radix: Int = DEFAULT_RADIX, - choices: Map, - override var validator: Validator = null + private val radix: Int = DEFAULT_RADIX, + choices: Map, + override var validator: Validator = null, ) : ChoiceConverter(choices) { - override val signatureTypeString: String = "converters.number.signatureType" + override val signatureTypeString: String = "converters.number.signatureType" - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - try { - this.parsed = arg.toLong(radix) - } catch (e: NumberFormatException) { - val errorString = if (radix == DEFAULT_RADIX) { - context.translate("converters.number.error.invalid.defaultBase", replacements = arrayOf(arg)) - } else { - context.translate("converters.number.error.invalid.otherBase", replacements = arrayOf(arg, radix)) - } + try { + this.parsed = arg.toLong(radix) + } catch (e: NumberFormatException) { + val errorString = if (radix == DEFAULT_RADIX) { + context.translate("converters.number.error.invalid.defaultBase", replacements = arrayOf(arg)) + } else { + context.translate("converters.number.error.invalid.otherBase", replacements = arrayOf(arg, radix)) + } - throw DiscordRelayedException(errorString) - } + throw DiscordRelayedException(errorString) + } - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - IntegerOptionBuilder(arg.displayName, arg.description).apply { - required = true + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + IntegerOptionBuilder(arg.displayName, arg.description).apply { + required = true - this@NumberChoiceConverter.choices.forEach { choice(it.key, it.value) } - } + this@NumberChoiceConverter.choices.forEach { choice(it.key, it.value) } + } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? IntegerOptionValue)?.value ?: return false - this.parsed = optionValue + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? IntegerOptionValue)?.value ?: return false + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/StringChoiceConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/StringChoiceConverter.kt index 98dbfe5f19..d3aadf9a83 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/StringChoiceConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/slash/converters/impl/StringChoiceConverter.kt @@ -22,35 +22,35 @@ import dev.kord.rest.builder.interaction.StringChoiceBuilder * Choice converter for string arguments. Supports mapping up to 25 choices to string. */ @Converter( - "string", + "string", - types = [ConverterType.CHOICE, ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE] + types = [ConverterType.CHOICE, ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE] ) public class StringChoiceConverter( - choices: Map, - override var validator: Validator = null + choices: Map, + override var validator: Validator = null, ) : ChoiceConverter(choices) { - override val signatureTypeString: String = "converters.string.signatureType" + override val signatureTypeString: String = "converters.string.signatureType" - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - this.parsed = arg + this.parsed = arg - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { - required = true + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { + required = true - this@StringChoiceConverter.choices.forEach { choice(it.key, it.value) } - } + this@StringChoiceConverter.choices.forEach { choice(it.key, it.value) } + } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - this.parsed = optionValue + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/EphemeralUserCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/EphemeralUserCommand.kt index 7f28f2a8a1..96972e81cf 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/EphemeralUserCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/EphemeralUserCommand.kt @@ -27,120 +27,120 @@ import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialEphemeralUserResponseBuilder = - (suspend InteractionResponseCreateBuilder.(UserCommandInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(UserCommandInteractionCreateEvent) -> Unit)? /** Ephemeral user command. **/ public class EphemeralUserCommand( - extension: Extension, - public override val modal: (() -> M)? = null, + extension: Extension, + public override val modal: (() -> M)? = null, ) : UserCommand, M>(extension) { - /** @suppress Internal guilder **/ - public var initialResponseBuilder: InitialEphemeralUserResponseBuilder = null + /** @suppress Internal guilder **/ + public var initialResponseBuilder: InitialEphemeralUserResponseBuilder = null - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialEphemeralUserResponseBuilder) { - initialResponseBuilder = body - } - - override fun validate() { - super.validate() - - if (modal != null && initialResponseBuilder != null) { - throw InvalidCommandException( - name, - - "You may not provide a modal builder and an initial response - pick one, not both." - ) - } - } - - override suspend fun call(event: UserCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { - emitEventAsync(EphemeralUserCommandInvocationEvent(this, event)) - - try { - if (!runChecks(event, cache)) { - emitEventAsync( - EphemeralUserCommandFailedChecksEvent( - this, - event, - "Checks failed without a message." - ) - ) - - return - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - emitEventAsync(EphemeralUserCommandFailedChecksEvent(this, event, e.reason)) - - return - } - - val modalObj = modal?.invoke() - - val response = if (initialResponseBuilder != null) { - event.interaction.respondEphemeral { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) - - val locale = event.getLocale() - - event.interaction.modal( - modalObj.translateTitle(locale, resolvedBundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) - } - - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) - - it?.deferEphemeralResponseUnsafe() - } ?: return - } else { - event.interaction.deferEphemeralResponseUnsafe() - } - - val context = EphemeralUserCommandContext(event, this, response, cache) - - context.populate() - - firstSentryBreadcrumb(context) - - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - emitEventAsync(EphemeralUserCommandFailedChecksEvent(this, event, e.reason)) - - return - } - - try { - body(context, modalObj) - } catch (t: Throwable) { - emitEventAsync(EphemeralUserCommandFailedWithExceptionEvent(this, event, t)) - - if (t is DiscordRelayedException) { - respondText(context, t.reason, FailureReason.RelayedFailure(t)) - - return - } - - handleError(context, t) - } - - emitEventAsync(EphemeralUserCommandSucceededEvent(this, event)) - } - - override suspend fun respondText( - context: EphemeralUserCommandContext, - message: String, - failureType: FailureReason<*> - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialEphemeralUserResponseBuilder) { + initialResponseBuilder = body + } + + override fun validate() { + super.validate() + + if (modal != null && initialResponseBuilder != null) { + throw InvalidCommandException( + name, + + "You may not provide a modal builder and an initial response - pick one, not both." + ) + } + } + + override suspend fun call(event: UserCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { + emitEventAsync(EphemeralUserCommandInvocationEvent(this, event)) + + try { + if (!runChecks(event, cache)) { + emitEventAsync( + EphemeralUserCommandFailedChecksEvent( + this, + event, + "Checks failed without a message." + ) + ) + + return + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + emitEventAsync(EphemeralUserCommandFailedChecksEvent(this, event, e.reason)) + + return + } + + val modalObj = modal?.invoke() + + val response = if (initialResponseBuilder != null) { + event.interaction.respondEphemeral { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) + + val locale = event.getLocale() + + event.interaction.modal( + modalObj.translateTitle(locale, resolvedBundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) + } + + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) + + it?.deferEphemeralResponseUnsafe() + } ?: return + } else { + event.interaction.deferEphemeralResponseUnsafe() + } + + val context = EphemeralUserCommandContext(event, this, response, cache) + + context.populate() + + firstSentryBreadcrumb(context) + + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + emitEventAsync(EphemeralUserCommandFailedChecksEvent(this, event, e.reason)) + + return + } + + try { + body(context, modalObj) + } catch (t: Throwable) { + emitEventAsync(EphemeralUserCommandFailedWithExceptionEvent(this, event, t)) + + if (t is DiscordRelayedException) { + respondText(context, t.reason, FailureReason.RelayedFailure(t)) + + return + } + + handleError(context, t) + } + + emitEventAsync(EphemeralUserCommandSucceededEvent(this, event)) + } + + override suspend fun respondText( + context: EphemeralUserCommandContext, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/EphemeralUserCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/EphemeralUserCommandContext.kt index f35bb9a213..0506b0324d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/EphemeralUserCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/EphemeralUserCommandContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent /** Ephemeral-only user command context. **/ public class EphemeralUserCommandContext( - override val event: UserCommandInteractionCreateEvent, - override val command: UserCommand, M>, - override val interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap + override val event: UserCommandInteractionCreateEvent, + override val command: UserCommand, M>, + override val interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : UserCommandContext, M>(event, command, cache), EphemeralInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/PublicUserCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/PublicUserCommand.kt index ff90bedea4..7fa981e8f0 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/PublicUserCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/PublicUserCommand.kt @@ -28,120 +28,120 @@ import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialPublicUserResponseBuilder = - (suspend InteractionResponseCreateBuilder.(UserCommandInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(UserCommandInteractionCreateEvent) -> Unit)? /** Public user command. **/ public class PublicUserCommand( - extension: Extension, - public override val modal: (() -> M)? = null, + extension: Extension, + public override val modal: (() -> M)? = null, ) : UserCommand, M>(extension) { - /** @suppress Internal guilder **/ - public var initialResponseBuilder: InitialPublicUserResponseBuilder = null + /** @suppress Internal guilder **/ + public var initialResponseBuilder: InitialPublicUserResponseBuilder = null - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialPublicUserResponseBuilder) { - initialResponseBuilder = body - } - - override fun validate() { - super.validate() - - if (modal != null && initialResponseBuilder != null) { - throw InvalidCommandException( - name, - - "You may not provide a modal builder and an initial response - pick one, not both." - ) - } - } - - override suspend fun call(event: UserCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { - emitEventAsync(PublicUserCommandInvocationEvent(this, event)) - - try { - if (!runChecks(event, cache)) { - emitEventAsync( - PublicUserCommandFailedChecksEvent( - this, - event, - "Checks failed without a message." - ) - ) - - return - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - emitEventAsync(PublicUserCommandFailedChecksEvent(this, event, e.reason)) - - return - } - - val modalObj = modal?.invoke() - - val response = if (initialResponseBuilder != null) { - event.interaction.respondPublic { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) - - val locale = event.getLocale() - - event.interaction.modal( - modalObj.translateTitle(locale, resolvedBundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) - } - - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) - - it?.deferPublicResponseUnsafe() - } ?: return - } else { - event.interaction.deferPublicResponseUnsafe() - } - - val context = PublicUserCommandContext(event, this, response, cache) - - context.populate() - - firstSentryBreadcrumb(context) - - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - emitEventAsync(PublicUserCommandFailedChecksEvent(this, event, e.reason)) - - return - } - - try { - body(context, modalObj) - } catch (t: Throwable) { - emitEventAsync(PublicUserCommandFailedWithExceptionEvent(this, event, t)) - - if (t is DiscordRelayedException) { - respondText(context, t.reason, FailureReason.RelayedFailure(t)) - - return - } - - handleError(context, t) - } - - emitEventAsync(PublicUserCommandSucceededEvent(this, event)) - } - - override suspend fun respondText( - context: PublicUserCommandContext, - message: String, - failureType: FailureReason<*> - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialPublicUserResponseBuilder) { + initialResponseBuilder = body + } + + override fun validate() { + super.validate() + + if (modal != null && initialResponseBuilder != null) { + throw InvalidCommandException( + name, + + "You may not provide a modal builder and an initial response - pick one, not both." + ) + } + } + + override suspend fun call(event: UserCommandInteractionCreateEvent, cache: MutableStringKeyedMap) { + emitEventAsync(PublicUserCommandInvocationEvent(this, event)) + + try { + if (!runChecks(event, cache)) { + emitEventAsync( + PublicUserCommandFailedChecksEvent( + this, + event, + "Checks failed without a message." + ) + ) + + return + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + emitEventAsync(PublicUserCommandFailedChecksEvent(this, event, e.reason)) + + return + } + + val modalObj = modal?.invoke() + + val response = if (initialResponseBuilder != null) { + event.interaction.respondPublic { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) + + val locale = event.getLocale() + + event.interaction.modal( + modalObj.translateTitle(locale, resolvedBundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), resolvedBundle) + } + + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) + + it?.deferPublicResponseUnsafe() + } ?: return + } else { + event.interaction.deferPublicResponseUnsafe() + } + + val context = PublicUserCommandContext(event, this, response, cache) + + context.populate() + + firstSentryBreadcrumb(context) + + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + emitEventAsync(PublicUserCommandFailedChecksEvent(this, event, e.reason)) + + return + } + + try { + body(context, modalObj) + } catch (t: Throwable) { + emitEventAsync(PublicUserCommandFailedWithExceptionEvent(this, event, t)) + + if (t is DiscordRelayedException) { + respondText(context, t.reason, FailureReason.RelayedFailure(t)) + + return + } + + handleError(context, t) + } + + emitEventAsync(PublicUserCommandSucceededEvent(this, event)) + } + + override suspend fun respondText( + context: PublicUserCommandContext, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/PublicUserCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/PublicUserCommandContext.kt index f858b2ca6d..07b05feb53 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/PublicUserCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/PublicUserCommandContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent /** Public-only user command context. **/ public class PublicUserCommandContext( - override val event: UserCommandInteractionCreateEvent, - override val command: UserCommand, M>, - override val interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap + override val event: UserCommandInteractionCreateEvent, + override val command: UserCommand, M>, + override val interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : UserCommandContext, M>(event, command, cache), PublicInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/UserCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/UserCommand.kt index 6d7b73f20a..54df9d120d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/UserCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/UserCommand.kt @@ -27,44 +27,44 @@ import org.koin.core.component.inject * @param modal Callable returning a `ModalForm` object, if any */ public abstract class UserCommand, M : ModalForm>( - extension: Extension, - public open val modal: (() -> M)? = null, + extension: Extension, + public open val modal: (() -> M)? = null, ) : ApplicationCommand(extension) { - private val logger: KLogger = KotlinLogging.logger {} + private val logger: KLogger = KotlinLogging.logger {} - /** @suppress This is only meant for use by code that extends the command system. **/ - public val componentRegistry: ComponentRegistry by inject() + /** @suppress This is only meant for use by code that extends the command system. **/ + public val componentRegistry: ComponentRegistry by inject() - /** Command body, to be called when the command is executed. **/ - public lateinit var body: suspend C.(M?) -> Unit + /** Command body, to be called when the command is executed. **/ + public lateinit var body: suspend C.(M?) -> Unit - override val type: ApplicationCommandType = ApplicationCommandType.User + override val type: ApplicationCommandType = ApplicationCommandType.User - /** Call this to supply a command [body], to be called when the command is executed. **/ - public fun action(action: suspend C.(M?) -> Unit) { - body = action - } + /** Call this to supply a command [body], to be called when the command is executed. **/ + public fun action(action: suspend C.(M?) -> Unit) { + body = action + } - override fun validate() { - super.validate() + override fun validate() { + super.validate() - if (!::body.isInitialized) { - throw InvalidCommandException(name, "No command body given.") - } - } + if (!::body.isInitialized) { + throw InvalidCommandException(name, "No command body given.") + } + } - /** Override this to implement your command's calling logic. Check subtypes for examples! **/ - public abstract override suspend fun call( - event: UserCommandInteractionCreateEvent, - cache: MutableStringKeyedMap - ) + /** Override this to implement your command's calling logic. Check subtypes for examples! **/ + public abstract override suspend fun call( + event: UserCommandInteractionCreateEvent, + cache: MutableStringKeyedMap, + ) - /** Override this to implement a way to respond to the user, regardless of whatever happens. **/ - public abstract suspend fun respondText(context: C, message: String, failureType: FailureReason<*>) + /** Override this to implement a way to respond to the user, regardless of whatever happens. **/ + public abstract suspend fun respondText(context: C, message: String, failureType: FailureReason<*>) - /** If enabled, adds the initial Sentry breadcrumb to the given context. **/ - public open suspend fun firstSentryBreadcrumb(context: C) { - if (sentry.enabled) { + /** If enabled, adds the initial Sentry breadcrumb to the given context. **/ + public open suspend fun firstSentryBreadcrumb(context: C) { + if (sentry.enabled) { context.sentry.context( "command", @@ -78,66 +78,66 @@ public abstract class UserCommand, M : ModalForm>( "extension", extension.name ) - context.sentry.breadcrumb(BreadcrumbType.User) { - category = "command.application.user" - message = "User command \"$name\" called." + context.sentry.breadcrumb(BreadcrumbType.User) { + category = "command.application.user" + message = "User command \"$name\" called." - channel = context.channel.asChannelOrNull() - guild = context.guild?.asGuildOrNull() + channel = context.channel.asChannelOrNull() + guild = context.guild?.asGuildOrNull() - data["command"] = name - } - } - } + data["command"] = name + } + } + } - override suspend fun runChecks( - event: UserCommandInteractionCreateEvent, - cache: MutableStringKeyedMap - ): Boolean { - val locale = event.getLocale() - val result = super.runChecks(event, cache) + override suspend fun runChecks( + event: UserCommandInteractionCreateEvent, + cache: MutableStringKeyedMap, + ): Boolean { + val locale = event.getLocale() + val result = super.runChecks(event, cache) - if (result) { - settings.applicationCommandsBuilder.userCommandChecks.forEach { check -> - val context = CheckContextWithCache(event, locale, cache) + if (result) { + settings.applicationCommandsBuilder.userCommandChecks.forEach { check -> + val context = CheckContextWithCache(event, locale, cache) - check(context) + check(context) - if (!context.passed) { - context.throwIfFailedWithMessage() + if (!context.passed) { + context.throwIfFailedWithMessage() - return false - } - } + return false + } + } - extension.userCommandChecks.forEach { check -> - val context = CheckContextWithCache(event, locale, cache) + extension.userCommandChecks.forEach { check -> + val context = CheckContextWithCache(event, locale, cache) - check(context) + check(context) - if (!context.passed) { - context.throwIfFailedWithMessage() + if (!context.passed) { + context.throwIfFailedWithMessage() - return false - } - } - } + return false + } + } + } - return result - } + return result + } - /** A general way to handle errors thrown during the course of a command's execution. **/ + /** A general way to handle errors thrown during the course of a command's execution. **/ @Suppress("StringLiteralDuplication") public open suspend fun handleError(context: C, t: Throwable) { - logger.error(t) { "Error during execution of $name user command (${context.event})" } + logger.error(t) { "Error during execution of $name user command (${context.event})" } - if (sentry.enabled) { - logger.trace { "Submitting error to sentry." } + if (sentry.enabled) { + logger.trace { "Submitting error to sentry." } - val sentryId = context.sentry.captureThrowable(t) { + val sentryId = context.sentry.captureThrowable(t) { user = context.user.asUserOrNull() channel = context.channel.asChannelOrNull() - } + } val errorMessage = if (sentryId != null) { logger.info { "Error submitted to Sentry: $sentryId" } @@ -152,12 +152,12 @@ public abstract class UserCommand, M : ModalForm>( } respondText(context, errorMessage, FailureReason.ExecutionError(t)) - } else { - respondText( - context, - context.translate("commands.error.user", null), - FailureReason.ExecutionError(t) - ) - } - } + } else { + respondText( + context, + context.translate("commands.error.user", null), + FailureReason.ExecutionError(t) + ) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/UserCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/UserCommandContext.kt index 14edce171d..bc7bd9d155 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/UserCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/user/UserCommandContext.kt @@ -19,10 +19,10 @@ import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent * @param command Message command instance. */ public abstract class UserCommandContext, M : ModalForm>( - public open val event: UserCommandInteractionCreateEvent, - public override val command: UserCommand, - cache: MutableStringKeyedMap + public open val event: UserCommandInteractionCreateEvent, + public override val command: UserCommand, + cache: MutableStringKeyedMap, ) : ApplicationCommandContext(event, command, cache) { - /** Messages that this message command is being executed against. **/ - public val targetUsers: Collection by lazy { event.interaction.users.values } + /** Messages that this message command is being executed against. **/ + public val targetUsers: Collection by lazy { event.interaction.users.values } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommand.kt index 3510fc753a..52fe892708 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommand.kt @@ -43,336 +43,336 @@ private val logger = KotlinLogging.logger {} */ @ExtensionDSL public open class ChatCommand( - extension: Extension, - public open val arguments: (() -> T)? = null, + extension: Extension, + public open val arguments: (() -> T)? = null, ) : Command(extension) { - /** Whether to allow the parser to parse keyword arguments. Defaults to `true`. **/ - public open var allowKeywordArguments: Boolean = true - - /** - * @suppress - */ - public open lateinit var body: suspend ChatCommandContext.() -> Unit - - /** - * A description of what this function and how it's intended to be used. - * - * This is intended to be made use of by help commands. - */ - public open var description: String = "commands.defaultDescription" - - /** - * Whether this command is enabled and can be invoked. - * - * Disabled commands cannot be invoked, and won't be shown in help commands. - * - * This can be changed at runtime, if commands need to be enabled and disabled dynamically without being - * reconstructed. - */ - public open var enabled: Boolean = true - - /** - * Whether to hide this command from help command listings. - * - * By default, this is `false` - so the command will be shown. - */ - public open var hidden: Boolean = false - - /** - * When translated, whether this command supports locale fallback when a user is trying to resolve a command by - * name in a locale other than the bot's configured default locale. - * - * If you'd like your command to be accessible in English along with the other languages the locale resolvers may - * have set up, turn this on. - */ - public open var localeFallback: Boolean = false - - /** - * Alternative names that can be used to invoke your command. - * - * There's no limit on the number of aliases a command may have, but in the event of an alias matching - * the [name] of a registered command, the command with the [name] takes priority. - */ - public open var aliases: Array = arrayOf() - - /** - * Translation key referencing a comma-separated list of command aliases. - * - * If this is set, the [aliases] list is ignored. This is also slightly more efficient during the first - * translation pass, as only one key will ever need to be translated. - */ - public open var aliasKey: String? = null - - /** - * @suppress - */ - public open val checkList: MutableList = mutableListOf() - - /** Translation cache, so we don't have to look up translations every time. **/ - public open val aliasTranslationCache: MutableMap> = mutableMapOf() - - /** Provide a translation key here to replace the auto-generated signature string. **/ - public open var signature: String? = null - - /** Locale-based cache of generated signature strings. **/ - public open var signatureCache: MutableMap = mutableMapOf() - - /** Chat command registry. **/ - public val registry: ChatCommandRegistry by inject() - - /** - * Retrieve the command signature for a locale, which specifies how the command's arguments should be structured. - * - * Command signatures are generated automatically by the [ChatCommandParser]. - */ - public open suspend fun getSignature(locale: Locale): String { - if (this.arguments == null) { - return "" - } - - if (!signatureCache.containsKey(locale)) { - if (signature != null) { - signatureCache[locale] = translationsProvider.translate( - signature!!, - resolvedBundle, - locale - ) - } else { - signatureCache[locale] = registry.parser.signature(arguments!!, locale) - } - } - - return signatureCache[locale]!! - } - - /** Return this command's name translated for the given locale, cached as required. **/ - public open fun getTranslatedName(locale: Locale): String { - if (!nameTranslationCache.containsKey(locale)) { - nameTranslationCache[locale] = translationsProvider.translate( - this.name, - this.resolvedBundle, - locale - ).lowercase() - } - - return nameTranslationCache[locale]!! - } - - /** Return this command's aliases translated for the given locale, cached as required. **/ - public open fun getTranslatedAliases(locale: Locale): Set { - if (!aliasTranslationCache.containsKey(locale)) { - val translations = if (aliasKey != null) { - translationsProvider.translate(aliasKey!!, resolvedBundle, locale) - .lowercase() - .split(",") - .map { it.trim() } - .filter { it != EMPTY_VALUE_STRING } - .toSortedSet() - } else { - this.aliases.map { - translationsProvider.translate(it, resolvedBundle, locale).lowercase() - }.toSortedSet() - } - - aliasTranslationCache[locale] = translations - } - - return aliasTranslationCache[locale]!! - } - - /** - * An internal function used to ensure that all of a command's required arguments are present. - * - * @throws InvalidCommandException Thrown when a required argument hasn't been set. - */ - @Throws(InvalidCommandException::class) - public override fun validate() { - super.validate() - - if (!::body.isInitialized) { - throw InvalidCommandException(name, "No command action given.") - } - } - - // region: DSL functions - - /** - * Define what will happen when your command is invoked. - * - * @param action The body of your command, which will be executed when your command is invoked. - */ - public open fun action(action: suspend ChatCommandContext.() -> Unit) { - this.body = action - } - - /** - * Define a check which must pass for the command to be executed. - * - * A command may have multiple checks - all checks must pass for the command to be executed. - * Checks will be run in the order that they're defined. - * - * This function can be used DSL-style with a given body, or it can be passed one or more - * predefined functions. See the samples for more information. - * - * @param checks Checks to apply to this command. - */ - public open fun check(vararg checks: ChatCommandCheck) { - checks.forEach { checkList.add(it) } - } - - /** - * Overloaded check function to allow for DSL syntax. - * - * @param check Check to apply to this command. - */ - public open fun check(check: ChatCommandCheck) { - checkList.add(check) - } - - // endregion - - /** Run checks with the provided [MessageCreateEvent]. Return false if any failed, true otherwise. **/ - public open suspend fun runChecks( - event: MessageCreateEvent, - sendMessage: Boolean = true, - cache: MutableStringKeyedMap, - ): Boolean { - val locale = event.getLocale() - - // global command checks - for (check in extension.bot.settings.chatCommandsBuilder.checkList) { - val context = CheckContextWithCache(event, locale, cache) - - check(context) - - if (!context.passed) { - val message = context.getTranslatedMessage() - - if (message != null && sendMessage) { - event.message.respond { - settings.failureResponseBuilder( - this, - message, - - FailureReason.ProvidedCheckFailure( - DiscordRelayedException(message, context.errorResponseKey) - ) - ) - } - } - - return false - } - } - - // local extension checks - for (check in extension.chatCommandChecks) { - val context = CheckContextWithCache(event, locale, cache) - - check(context) - - if (!context.passed) { - val message = context.message - - if (message != null && sendMessage) { - event.message.respond { - settings.failureResponseBuilder( - this, - message, - - FailureReason.ProvidedCheckFailure( - DiscordRelayedException(message, context.errorResponseKey) - ) - ) - } - } - - return false - } - } - - for (check in checkList) { - val context = CheckContextWithCache(event, locale, cache) - - check(context) - - if (!context.passed) { - val message = context.message - - if (message != null && sendMessage) { - event.message.respond { - settings.failureResponseBuilder( - this, - message, - - FailureReason.ProvidedCheckFailure( - DiscordRelayedException(message, context.errorResponseKey) - ) - ) - } - } - - return false - } - } - - return true - } - - /** - * Execute this command, given a [MessageCreateEvent]. - * - * This function takes a [MessageCreateEvent] (generated when a message is received), and - * processes it. The command's checks are invoked and, assuming all of the - * checks passed, the [command body][action] is executed. - * - * If an exception is thrown by the [command body][action], it is caught and a traceback - * is printed. - * - * @param event The message creation event. - * @param commandName The name used to invoke this command. - * @param parser Parser used to parse the command's arguments, available for further parsing. - * @param argString Original string containing the command's arguments. - * @param skipChecks Whether to skip testing the command's checks. - */ - public open suspend fun call( - event: MessageCreateEvent, - commandName: String, - parser: StringParser, - argString: String, - skipChecks: Boolean = false, - cache: MutableStringKeyedMap = mutableMapOf() - ): Unit = withLock { - emitEventAsync(ChatCommandInvocationEvent(this, event)) - - try { - if (!skipChecks && !runChecks(event, cache = cache)) { - emitEventAsync( - ChatCommandFailedChecksEvent( - this, - event, - "Checks failed without a message." - ) - ) - - return@withLock - } - } catch (e: DiscordRelayedException) { - emitEventAsync(ChatCommandFailedChecksEvent(this, event, e.reason)) - - event.message.respond { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - return@withLock - } - - val context = ChatCommandContext(this, event, commandName, parser, argString, cache) - - context.populate() - - if (sentry.enabled) { + /** Whether to allow the parser to parse keyword arguments. Defaults to `true`. **/ + public open var allowKeywordArguments: Boolean = true + + /** + * @suppress + */ + public open lateinit var body: suspend ChatCommandContext.() -> Unit + + /** + * A description of what this function and how it's intended to be used. + * + * This is intended to be made use of by help commands. + */ + public open var description: String = "commands.defaultDescription" + + /** + * Whether this command is enabled and can be invoked. + * + * Disabled commands cannot be invoked, and won't be shown in help commands. + * + * This can be changed at runtime, if commands need to be enabled and disabled dynamically without being + * reconstructed. + */ + public open var enabled: Boolean = true + + /** + * Whether to hide this command from help command listings. + * + * By default, this is `false` - so the command will be shown. + */ + public open var hidden: Boolean = false + + /** + * When translated, whether this command supports locale fallback when a user is trying to resolve a command by + * name in a locale other than the bot's configured default locale. + * + * If you'd like your command to be accessible in English along with the other languages the locale resolvers may + * have set up, turn this on. + */ + public open var localeFallback: Boolean = false + + /** + * Alternative names that can be used to invoke your command. + * + * There's no limit on the number of aliases a command may have, but in the event of an alias matching + * the [name] of a registered command, the command with the [name] takes priority. + */ + public open var aliases: Array = arrayOf() + + /** + * Translation key referencing a comma-separated list of command aliases. + * + * If this is set, the [aliases] list is ignored. This is also slightly more efficient during the first + * translation pass, as only one key will ever need to be translated. + */ + public open var aliasKey: String? = null + + /** + * @suppress + */ + public open val checkList: MutableList = mutableListOf() + + /** Translation cache, so we don't have to look up translations every time. **/ + public open val aliasTranslationCache: MutableMap> = mutableMapOf() + + /** Provide a translation key here to replace the auto-generated signature string. **/ + public open var signature: String? = null + + /** Locale-based cache of generated signature strings. **/ + public open var signatureCache: MutableMap = mutableMapOf() + + /** Chat command registry. **/ + public val registry: ChatCommandRegistry by inject() + + /** + * Retrieve the command signature for a locale, which specifies how the command's arguments should be structured. + * + * Command signatures are generated automatically by the [ChatCommandParser]. + */ + public open suspend fun getSignature(locale: Locale): String { + if (this.arguments == null) { + return "" + } + + if (!signatureCache.containsKey(locale)) { + if (signature != null) { + signatureCache[locale] = translationsProvider.translate( + signature!!, + resolvedBundle, + locale + ) + } else { + signatureCache[locale] = registry.parser.signature(arguments!!, locale) + } + } + + return signatureCache[locale]!! + } + + /** Return this command's name translated for the given locale, cached as required. **/ + public open fun getTranslatedName(locale: Locale): String { + if (!nameTranslationCache.containsKey(locale)) { + nameTranslationCache[locale] = translationsProvider.translate( + this.name, + this.resolvedBundle, + locale + ).lowercase() + } + + return nameTranslationCache[locale]!! + } + + /** Return this command's aliases translated for the given locale, cached as required. **/ + public open fun getTranslatedAliases(locale: Locale): Set { + if (!aliasTranslationCache.containsKey(locale)) { + val translations = if (aliasKey != null) { + translationsProvider.translate(aliasKey!!, resolvedBundle, locale) + .lowercase() + .split(",") + .map { it.trim() } + .filter { it != EMPTY_VALUE_STRING } + .toSortedSet() + } else { + this.aliases.map { + translationsProvider.translate(it, resolvedBundle, locale).lowercase() + }.toSortedSet() + } + + aliasTranslationCache[locale] = translations + } + + return aliasTranslationCache[locale]!! + } + + /** + * An internal function used to ensure that all of a command's required arguments are present. + * + * @throws InvalidCommandException Thrown when a required argument hasn't been set. + */ + @Throws(InvalidCommandException::class) + public override fun validate() { + super.validate() + + if (!::body.isInitialized) { + throw InvalidCommandException(name, "No command action given.") + } + } + + // region: DSL functions + + /** + * Define what will happen when your command is invoked. + * + * @param action The body of your command, which will be executed when your command is invoked. + */ + public open fun action(action: suspend ChatCommandContext.() -> Unit) { + this.body = action + } + + /** + * Define a check which must pass for the command to be executed. + * + * A command may have multiple checks - all checks must pass for the command to be executed. + * Checks will be run in the order that they're defined. + * + * This function can be used DSL-style with a given body, or it can be passed one or more + * predefined functions. See the samples for more information. + * + * @param checks Checks to apply to this command. + */ + public open fun check(vararg checks: ChatCommandCheck) { + checks.forEach { checkList.add(it) } + } + + /** + * Overloaded check function to allow for DSL syntax. + * + * @param check Check to apply to this command. + */ + public open fun check(check: ChatCommandCheck) { + checkList.add(check) + } + + // endregion + + /** Run checks with the provided [MessageCreateEvent]. Return false if any failed, true otherwise. **/ + public open suspend fun runChecks( + event: MessageCreateEvent, + sendMessage: Boolean = true, + cache: MutableStringKeyedMap, + ): Boolean { + val locale = event.getLocale() + + // global command checks + for (check in extension.bot.settings.chatCommandsBuilder.checkList) { + val context = CheckContextWithCache(event, locale, cache) + + check(context) + + if (!context.passed) { + val message = context.getTranslatedMessage() + + if (message != null && sendMessage) { + event.message.respond { + settings.failureResponseBuilder( + this, + message, + + FailureReason.ProvidedCheckFailure( + DiscordRelayedException(message, context.errorResponseKey) + ) + ) + } + } + + return false + } + } + + // local extension checks + for (check in extension.chatCommandChecks) { + val context = CheckContextWithCache(event, locale, cache) + + check(context) + + if (!context.passed) { + val message = context.message + + if (message != null && sendMessage) { + event.message.respond { + settings.failureResponseBuilder( + this, + message, + + FailureReason.ProvidedCheckFailure( + DiscordRelayedException(message, context.errorResponseKey) + ) + ) + } + } + + return false + } + } + + for (check in checkList) { + val context = CheckContextWithCache(event, locale, cache) + + check(context) + + if (!context.passed) { + val message = context.message + + if (message != null && sendMessage) { + event.message.respond { + settings.failureResponseBuilder( + this, + message, + + FailureReason.ProvidedCheckFailure( + DiscordRelayedException(message, context.errorResponseKey) + ) + ) + } + } + + return false + } + } + + return true + } + + /** + * Execute this command, given a [MessageCreateEvent]. + * + * This function takes a [MessageCreateEvent] (generated when a message is received), and + * processes it. The command's checks are invoked and, assuming all of the + * checks passed, the [command body][action] is executed. + * + * If an exception is thrown by the [command body][action], it is caught and a traceback + * is printed. + * + * @param event The message creation event. + * @param commandName The name used to invoke this command. + * @param parser Parser used to parse the command's arguments, available for further parsing. + * @param argString Original string containing the command's arguments. + * @param skipChecks Whether to skip testing the command's checks. + */ + public open suspend fun call( + event: MessageCreateEvent, + commandName: String, + parser: StringParser, + argString: String, + skipChecks: Boolean = false, + cache: MutableStringKeyedMap = mutableMapOf(), + ): Unit = withLock { + emitEventAsync(ChatCommandInvocationEvent(this, event)) + + try { + if (!skipChecks && !runChecks(event, cache = cache)) { + emitEventAsync( + ChatCommandFailedChecksEvent( + this, + event, + "Checks failed without a message." + ) + ) + + return@withLock + } + } catch (e: DiscordRelayedException) { + emitEventAsync(ChatCommandFailedChecksEvent(this, event, e.reason)) + + event.message.respond { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + return@withLock + } + + val context = ChatCommandContext(this, event, commandName, parser, argString, cache) + + context.populate() + + if (sentry.enabled) { val translatedName = when (this) { is ChatSubCommand -> this.getFullTranslatedName(context.getLocale()) is ChatGroupCommand -> this.getFullTranslatedName(context.getLocale()) @@ -393,119 +393,119 @@ public open class ChatCommand( "extension", extension.name ) - context.sentry.breadcrumb(BreadcrumbType.User) { - category = "command.chat" - message = "Command \"$name\" called." + context.sentry.breadcrumb(BreadcrumbType.User) { + category = "command.chat" + message = "Command \"$name\" called." - channel = event.message.getChannelOrNull() - guild = event.message.getGuildOrNull() + channel = event.message.getChannelOrNull() + guild = event.message.getGuildOrNull() - data["arguments"] = argString + data["arguments"] = argString - data["message.id"] = event.message.id.toString() - data["message.content::arguments"] = event.message.content - } - } + data["message.id"] = event.message.id.toString() + data["message.content::arguments"] = event.message.content + } + } - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - event.message.respond { - settings.failureResponseBuilder(this, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - } + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + event.message.respond { + settings.failureResponseBuilder(this, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + } - emitEventAsync(ChatCommandFailedChecksEvent(this, event, e.reason)) + emitEventAsync(ChatCommandFailedChecksEvent(this, event, e.reason)) - return@withLock - } + return@withLock + } - if (this.arguments != null) { - try { - val parsedArgs = registry.parser.parse(this.arguments!!, context) - context.populateArgs(parsedArgs) - } catch (e: ArgumentParsingException) { - event.message.respond { - settings.failureResponseBuilder(this, e.reason, FailureReason.ArgumentParsingFailure(e)) - } + if (this.arguments != null) { + try { + val parsedArgs = registry.parser.parse(this.arguments!!, context) + context.populateArgs(parsedArgs) + } catch (e: ArgumentParsingException) { + event.message.respond { + settings.failureResponseBuilder(this, e.reason, FailureReason.ArgumentParsingFailure(e)) + } - emitEventAsync(ChatCommandFailedParsingEvent(this, event, e)) + emitEventAsync(ChatCommandFailedParsingEvent(this, event, e)) - return@withLock - } - } + return@withLock + } + } - try { - this.body(context) - } catch (t: Throwable) { - emitEventAsync(ChatCommandFailedWithExceptionEvent(this, event, t)) + try { + this.body(context) + } catch (t: Throwable) { + emitEventAsync(ChatCommandFailedWithExceptionEvent(this, event, t)) - if (t is DiscordRelayedException) { - event.message.respond { - settings.failureResponseBuilder(this, t.reason, FailureReason.RelayedFailure(t)) - } + if (t is DiscordRelayedException) { + event.message.respond { + settings.failureResponseBuilder(this, t.reason, FailureReason.RelayedFailure(t)) + } - return@withLock - } + return@withLock + } - if (sentry.enabled) { - logger.trace { "Submitting error to sentry." } + if (sentry.enabled) { + logger.trace { "Submitting error to sentry." } - val channel = event.message.getChannelOrNull() + val channel = event.message.getChannelOrNull() - val sentryId = context.sentry.captureThrowable(t) { + val sentryId = context.sentry.captureThrowable(t) { this.user = event.message.author this.channel = channel - } - - logger.info { "Error submitted to Sentry: $sentryId" } - - sentry.addEventId(sentryId) - - logger.error(t) { "Error during execution of $name command ($event)" } - - if (extension.bot.extensions.containsKey("sentry")) { - val prefix = registry.getPrefix(event) - - event.message.respond { - settings.failureResponseBuilder( - this, - - context.translate( - "commands.error.user.sentry.message", - null, - replacements = arrayOf( - prefix, - sentryId - ) - ), - - FailureReason.ExecutionError(t) - ) - } - } else { - event.message.respond { - settings.failureResponseBuilder( - this, - context.translate("commands.error.user", null), - FailureReason.ExecutionError(t) - ) - } - } - } else { - logger.error(t) { "Error during execution of $name command ($event)" } - - event.message.respond { - settings.failureResponseBuilder( - this, - context.translate("commands.error.user", null), - FailureReason.ExecutionError(t) - ) - } - } - - return@withLock - } - - emitEventAsync(ChatCommandSucceededEvent(this, event)) - } + } + + logger.info { "Error submitted to Sentry: $sentryId" } + + sentry.addEventId(sentryId) + + logger.error(t) { "Error during execution of $name command ($event)" } + + if (extension.bot.extensions.containsKey("sentry")) { + val prefix = registry.getPrefix(event) + + event.message.respond { + settings.failureResponseBuilder( + this, + + context.translate( + "commands.error.user.sentry.message", + null, + replacements = arrayOf( + prefix, + sentryId + ) + ), + + FailureReason.ExecutionError(t) + ) + } + } else { + event.message.respond { + settings.failureResponseBuilder( + this, + context.translate("commands.error.user", null), + FailureReason.ExecutionError(t) + ) + } + } + } else { + logger.error(t) { "Error during execution of $name command ($event)" } + + event.message.respond { + settings.failureResponseBuilder( + this, + context.translate("commands.error.user", null), + FailureReason.ExecutionError(t) + ) + } + } + + return@withLock + } + + emitEventAsync(ChatCommandSucceededEvent(this, event)) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandContext.kt index 7e00748b4d..b035910896 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandContext.kt @@ -35,107 +35,108 @@ import dev.kord.core.event.message.MessageCreateEvent */ @ExtensionDSL public open class ChatCommandContext( - public val chatCommand: ChatCommand, - eventObj: MessageCreateEvent, - commandName: String, - public open val parser: StringParser, - public val argString: String, - cache: MutableStringKeyedMap + public val chatCommand: ChatCommand, + eventObj: MessageCreateEvent, + commandName: String, + public open val parser: StringParser, + public val argString: String, + cache: MutableStringKeyedMap, ) : CommandContext(chatCommand, eventObj, commandName, cache) { - /** Event that triggered this command execution. **/ - public val event: MessageCreateEvent get() = eventObj as MessageCreateEvent - - /** Message channel this command happened in, if any. **/ - public open lateinit var channel: MessageChannelBehavior - - /** Guild this command happened in, if any. **/ - public open var guild: GuildBehavior? = null - - /** Guild member responsible for executing this command, if any. **/ - public open var member: MemberBehavior? = null - - /** User responsible for executing this command, if any (if `null`, it's a webhook). **/ - public open var user: UserBehavior? = null - - /** Message object containing this command invocation. **/ - public open lateinit var message: Message - - /** Arguments object containing this command's parsed arguments. **/ - public open lateinit var arguments: T - - override suspend fun populate() { - channel = getChannel() - guild = getGuild() - member = getMember() - user = getUser() - - message = getMessage() - } - - /** @suppress Internal function **/ - public fun populateArgs(args: T) { - arguments = args - } - - override suspend fun getChannel(): MessageChannelBehavior = event.message.channel - override suspend fun getGuild(): GuildBehavior? = event.guildId - ?.let { event.kord.unsafe.guild(it) } - override suspend fun getMember(): MemberBehavior? = event.member - override suspend fun getUser(): UserBehavior? = event.message.author - - /** Extract message information from event data, if that context is available. **/ - public open suspend fun getMessage(): Message = event.message - - /** - * Convenience function to create a button paginator using a builder DSL syntax. Handles the contextual stuff for - * you. - */ - public suspend fun paginator( - defaultGroup: String = "", - - pingInReply: Boolean = true, - targetChannel: MessageChannelBehavior? = null, - targetMessage: Message? = null, - - body: suspend PaginatorBuilder.() -> Unit - ): MessageButtonPaginator { - val builder = PaginatorBuilder(getLocale(), defaultGroup = defaultGroup) - - body(builder) - - return MessageButtonPaginator(pingInReply, targetChannel, targetMessage, builder) - } - - /** - * Generate and send the help embed for this command, using the first loaded extensions that implements - * [HelpProvider]. - * - * @return `true` if a help extension exists and help was sent, `false` otherwise. - */ - public suspend fun sendHelp(): Boolean { - val helpExtension = this.command.extension.bot.findExtension() ?: return false - val paginator = helpExtension.getCommandHelpPaginator(this, chatCommand) - - paginator.send() - - return true - } - - /** - * Convenience function allowing for message responses with translated content. - */ - public suspend fun Message.respondTranslated( - key: String, - replacements: Array = arrayOf(), - useReply: Boolean = true - ): Message = respond(translate(key, command.resolvedBundle, replacements), useReply) - - /** - * Convenience function allowing for message responses with translated content. - */ - public suspend fun Message.respondTranslated( - key: String, - replacements: Map, - useReply: Boolean = true - ): Message = respond(translate(key, command.resolvedBundle, replacements), useReply) + /** Event that triggered this command execution. **/ + public val event: MessageCreateEvent get() = eventObj as MessageCreateEvent + + /** Message channel this command happened in, if any. **/ + public open lateinit var channel: MessageChannelBehavior + + /** Guild this command happened in, if any. **/ + public open var guild: GuildBehavior? = null + + /** Guild member responsible for executing this command, if any. **/ + public open var member: MemberBehavior? = null + + /** User responsible for executing this command, if any (if `null`, it's a webhook). **/ + public open var user: UserBehavior? = null + + /** Message object containing this command invocation. **/ + public open lateinit var message: Message + + /** Arguments object containing this command's parsed arguments. **/ + public open lateinit var arguments: T + + override suspend fun populate() { + channel = getChannel() + guild = getGuild() + member = getMember() + user = getUser() + + message = getMessage() + } + + /** @suppress Internal function **/ + public fun populateArgs(args: T) { + arguments = args + } + + override suspend fun getChannel(): MessageChannelBehavior = event.message.channel + override suspend fun getGuild(): GuildBehavior? = event.guildId + ?.let { event.kord.unsafe.guild(it) } + + override suspend fun getMember(): MemberBehavior? = event.member + override suspend fun getUser(): UserBehavior? = event.message.author + + /** Extract message information from event data, if that context is available. **/ + public open suspend fun getMessage(): Message = event.message + + /** + * Convenience function to create a button paginator using a builder DSL syntax. Handles the contextual stuff for + * you. + */ + public suspend fun paginator( + defaultGroup: String = "", + + pingInReply: Boolean = true, + targetChannel: MessageChannelBehavior? = null, + targetMessage: Message? = null, + + body: suspend PaginatorBuilder.() -> Unit, + ): MessageButtonPaginator { + val builder = PaginatorBuilder(getLocale(), defaultGroup = defaultGroup) + + body(builder) + + return MessageButtonPaginator(pingInReply, targetChannel, targetMessage, builder) + } + + /** + * Generate and send the help embed for this command, using the first loaded extensions that implements + * [HelpProvider]. + * + * @return `true` if a help extension exists and help was sent, `false` otherwise. + */ + public suspend fun sendHelp(): Boolean { + val helpExtension = this.command.extension.bot.findExtension() ?: return false + val paginator = helpExtension.getCommandHelpPaginator(this, chatCommand) + + paginator.send() + + return true + } + + /** + * Convenience function allowing for message responses with translated content. + */ + public suspend fun Message.respondTranslated( + key: String, + replacements: Array = arrayOf(), + useReply: Boolean = true, + ): Message = respond(translate(key, command.resolvedBundle, replacements), useReply) + + /** + * Convenience function allowing for message responses with translated content. + */ + public suspend fun Message.respondTranslated( + key: String, + replacements: Map, + useReply: Boolean = true, + ): Message = respond(translate(key, command.resolvedBundle, replacements), useReply) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandParser.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandParser.kt index 3cb3e459af..4f2623bfb4 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandParser.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatCommandParser.kt @@ -5,9 +5,9 @@ */ @file:Suppress( - "TooGenericExceptionCaught", - "StringLiteralDuplication", - "DuplicatedCode" + "TooGenericExceptionCaught", + "StringLiteralDuplication", + "DuplicatedCode" ) package com.kotlindiscord.kord.extensions.commands.chat @@ -45,863 +45,863 @@ private val logger = KotlinLogging.logger {} * We recommend reading over the source code if you'd like to get to grips with how this all works. */ public open class ChatCommandParser : KordExKoinComponent { - /** Current instance of the bot. **/ - public open val bot: ExtensibleBot by inject() - - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by inject() - - /** - * Given a builder returning an [Arguments] subclass and [CommandContext], parse the command's arguments into - * the [Arguments] subclass and return it. - * - * This is a fairly complex function. It works like this: - * - * 1. Enumerate all of the converters provided by the [Arguments] subclass, mapping them to their display names - * for easier keyword argument parsing. - * 2. Parse out the keyword arguments and store them in a map, leaving only the positional arguments in the list. - * 3. Loop over the converters, handing them the values they require. - * * If a converter has keyword arguments, use those instead of taking a value from the list of arguments. - * * For single, defaulting or optional converters, pass them a single argument and continue to the next. - * * For coalescing or multi converters: - * * If it's a keyword argument, pass it all of the keyed values and continue. - * * If it's positional, pass it all remaining positional arguments and remove those that were converted. - * - * @param builder Builder returning an [Arguments] subclass - usually the constructor. - * @param context MessageCommand context for this command invocation. - * - * @return Built [Arguments] object, with converters filled. - * @throws DiscordRelayedException Thrown based on a lot of possible cases. This is intended for display on Discord. - */ - public open suspend fun parse(builder: () -> T, context: ChatCommandContext<*>): T { - val argumentsObj = builder.invoke() - argumentsObj.validate() - - val parser = context.parser - - logger.trace { "Arguments object: $argumentsObj (${argumentsObj.args.size} args)" } - - val args = argumentsObj.args.toMutableList() - val argsMap = args.associateBy { it.displayName.lowercase() } - val keywordArgs: MutableStringKeyedMap> = mutableMapOf() - - if (context.chatCommand.allowKeywordArguments) { - parser.parseNamed().forEach { - val name = it.name.lowercase() - - keywordArgs[name] = keywordArgs[name] ?: mutableListOf() - keywordArgs[name]!!.add(it.data) - } - - logger.trace { "Parsed out ${keywordArgs.size} keyword args." } - } else { - logger.trace { "Skipping keyword args, command is configured to disallow them." } - } - - logger.trace { "Args map: $argsMap" } - - var currentArg: Argument<*>? - - @Suppress("LoopWithTooManyJumpStatements") // Listen here u lil shit - while (true) { - currentArg = args.removeFirstOrNull() - currentArg ?: break // If it's null, we're out of arguments - - val kwValue = keywordArgs[ - currentArg - .getDefaultTranslatedDisplayName(context.translationsProvider, context.command) - .lowercase(context.getLocale()) - ] - - val hasKwargs = kwValue != null - - logger.trace { "Current argument: ${currentArg.displayName}" } - logger.trace { "Keyword arg ($hasKwargs): $kwValue" } - - if (!parser.cursor.hasNext && !hasKwargs) { - continue - } - - when (val converter = currentArg.converter) { - is ChoiceConverter<*> -> error("Choice converters may only be used with slash commands") - - is SingleConverter<*> -> try { - val parsed = if (hasKwargs) { - if (kwValue!!.size != 1) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.requiresOneValue", - replacements = arrayOf(currentArg.displayName, kwValue.size) - ), - - "argumentParser.error.requiresOneValue", + /** Current instance of the bot. **/ + public open val bot: ExtensibleBot by inject() + + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by inject() + + /** + * Given a builder returning an [Arguments] subclass and [CommandContext], parse the command's arguments into + * the [Arguments] subclass and return it. + * + * This is a fairly complex function. It works like this: + * + * 1. Enumerate all of the converters provided by the [Arguments] subclass, mapping them to their display names + * for easier keyword argument parsing. + * 2. Parse out the keyword arguments and store them in a map, leaving only the positional arguments in the list. + * 3. Loop over the converters, handing them the values they require. + * * If a converter has keyword arguments, use those instead of taking a value from the list of arguments. + * * For single, defaulting or optional converters, pass them a single argument and continue to the next. + * * For coalescing or multi converters: + * * If it's a keyword argument, pass it all of the keyed values and continue. + * * If it's positional, pass it all remaining positional arguments and remove those that were converted. + * + * @param builder Builder returning an [Arguments] subclass - usually the constructor. + * @param context MessageCommand context for this command invocation. + * + * @return Built [Arguments] object, with converters filled. + * @throws DiscordRelayedException Thrown based on a lot of possible cases. This is intended for display on Discord. + */ + public open suspend fun parse(builder: () -> T, context: ChatCommandContext<*>): T { + val argumentsObj = builder.invoke() + argumentsObj.validate() + + val parser = context.parser + + logger.trace { "Arguments object: $argumentsObj (${argumentsObj.args.size} args)" } + + val args = argumentsObj.args.toMutableList() + val argsMap = args.associateBy { it.displayName.lowercase() } + val keywordArgs: MutableStringKeyedMap> = mutableMapOf() + + if (context.chatCommand.allowKeywordArguments) { + parser.parseNamed().forEach { + val name = it.name.lowercase() + + keywordArgs[name] = keywordArgs[name] ?: mutableListOf() + keywordArgs[name]!!.add(it.data) + } + + logger.trace { "Parsed out ${keywordArgs.size} keyword args." } + } else { + logger.trace { "Skipping keyword args, command is configured to disallow them." } + } + + logger.trace { "Args map: $argsMap" } + + var currentArg: Argument<*>? + + @Suppress("LoopWithTooManyJumpStatements") // Listen here u lil shit + while (true) { + currentArg = args.removeFirstOrNull() + currentArg ?: break // If it's null, we're out of arguments + + val kwValue = keywordArgs[ + currentArg + .getDefaultTranslatedDisplayName(context.translationsProvider, context.command) + .lowercase(context.getLocale()) + ] + + val hasKwargs = kwValue != null + + logger.trace { "Current argument: ${currentArg.displayName}" } + logger.trace { "Keyword arg ($hasKwargs): $kwValue" } + + if (!parser.cursor.hasNext && !hasKwargs) { + continue + } + + when (val converter = currentArg.converter) { + is ChoiceConverter<*> -> error("Choice converters may only be used with slash commands") + + is SingleConverter<*> -> try { + val parsed = if (hasKwargs) { + if (kwValue!!.size != 1) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.requiresOneValue", + replacements = arrayOf(currentArg.displayName, kwValue.size) + ), + + "argumentParser.error.requiresOneValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - converter.parse(parser, context, kwValue.first()) - } else { - converter.parse(parser, context) - } + converter.parse(parser, context, kwValue.first()) + } else { + converter.parse(parser, context) + } - if ((converter.required || hasKwargs) && !parsed) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.invalidValue", + if ((converter.required || hasKwargs) && !parsed) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.invalidValue", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - converter.getErrorString(context), - ) - ), + converter.getErrorString(context), + ) + ), - "argumentParser.error.invalidValue", + "argumentParser.error.invalidValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } - converter.parseSuccess = true + converter.parseSuccess = true - converter.validate(context) - } - } catch (e: DiscordRelayedException) { - if (converter.required || hasKwargs) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.errorInArgument", + converter.validate(context) + } + } catch (e: DiscordRelayedException) { + if (converter.required || hasKwargs) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.errorInArgument", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - converter.handleError(e, context) - ) - ), + converter.handleError(e, context) + ) + ), - "argumentParser.error.errorInArgument", + "argumentParser.error.errorInArgument", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required || hasKwargs) { - throw t - } - } - - is DefaultingConverter<*> -> try { - val parsed = if (hasKwargs) { - if (kwValue!!.size != 1) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.requiresOneValue", - replacements = arrayOf(currentArg.displayName, kwValue.size) - ), - - "argumentParser.error.requiresOneValue", + currentArg, + argumentsObj, + parser + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required || hasKwargs) { + throw t + } + } + + is DefaultingConverter<*> -> try { + val parsed = if (hasKwargs) { + if (kwValue!!.size != 1) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.requiresOneValue", + replacements = arrayOf(currentArg.displayName, kwValue.size) + ), + + "argumentParser.error.requiresOneValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - converter.parse(parser, context, kwValue.first()) - } else { - converter.parse(parser, context) - } + converter.parse(parser, context, kwValue.first()) + } else { + converter.parse(parser, context) + } - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } - converter.parseSuccess = true - } + converter.parseSuccess = true + } - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required || converter.outputError || hasKwargs) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.errorInArgument", + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required || converter.outputError || hasKwargs) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.errorInArgument", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - converter.handleError(e, context) - ) - ), + converter.handleError(e, context) + ) + ), - "argumentParser.error.errorInArgument", + "argumentParser.error.errorInArgument", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - } - - is OptionalConverter<*> -> try { - val parsed = if (hasKwargs) { - if (kwValue!!.size != 1) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.requiresOneValue", - replacements = arrayOf(currentArg.displayName, kwValue.size) - ), - - "argumentParser.error.requiresOneValue", + currentArg, + argumentsObj, + parser + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + } + + is OptionalConverter<*> -> try { + val parsed = if (hasKwargs) { + if (kwValue!!.size != 1) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.requiresOneValue", + replacements = arrayOf(currentArg.displayName, kwValue.size) + ), + + "argumentParser.error.requiresOneValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - converter.parse(parser, context, kwValue.first()) - } else { - converter.parse(parser, context) - } + converter.parse(parser, context, kwValue.first()) + } else { + converter.parse(parser, context) + } - if (parsed) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } + if (parsed) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } - converter.parseSuccess = true - } + converter.parseSuccess = true + } - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required || converter.outputError || hasKwargs) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.errorInArgument", + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required || converter.outputError || hasKwargs) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.errorInArgument", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - converter.handleError(e, context) - ) - ), + converter.handleError(e, context) + ) + ), - "argumentParser.error.errorInArgument", + "argumentParser.error.errorInArgument", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required || hasKwargs) { - throw t - } - } - - is ListConverter<*> -> try { - val parsedCount = if (hasKwargs) { - converter.parse(parser, context, kwValue!!) - } else { - converter.parse(parser, context) - } - - if ((converter.required || hasKwargs) && parsedCount <= 0) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.invalidValue", - - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), - - converter.getErrorString(context) - ) - ), - - "argumentParser.error.invalidValue", + currentArg, + argumentsObj, + parser + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required || hasKwargs) { + throw t + } + } + + is ListConverter<*> -> try { + val parsedCount = if (hasKwargs) { + converter.parse(parser, context, kwValue!!) + } else { + converter.parse(parser, context) + } + + if ((converter.required || hasKwargs) && parsedCount <= 0) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.invalidValue", + + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), + + converter.getErrorString(context) + ) + ), + + "argumentParser.error.invalidValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - if (hasKwargs) { - if (parsedCount < kwValue!!.size) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.notAllValid", + if (hasKwargs) { + if (parsedCount < kwValue!!.size) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.notAllValid", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = converter.bundle + ), - kwValue.size, - parsedCount, - context.translate(converter.signatureTypeString, bundleName = converter.bundle) - ) - ), + kwValue.size, + parsedCount, + context.translate(converter.signatureTypeString, bundleName = converter.bundle) + ) + ), - "argumentParser.error.notAllValid", + "argumentParser.error.notAllValid", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - converter.parseSuccess = true - } else { - if (parsedCount > 0) { - logger.trace { "Argument ${currentArg.displayName} successfully filled." } + converter.parseSuccess = true + } else { + if (parsedCount > 0) { + logger.trace { "Argument ${currentArg.displayName} successfully filled." } - converter.parseSuccess = true - } - } + converter.parseSuccess = true + } + } - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.errorInArgument", + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.errorInArgument", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - converter.handleError(e, context) - ) - ), + converter.handleError(e, context) + ) + ), - "argumentParser.error.errorInArgument", + "argumentParser.error.errorInArgument", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required) { - throw t - } - } - - is CoalescingConverter<*> -> try { - val parsedCount = if (hasKwargs) { - converter.parse(parser, context, kwValue!!) - } else { - converter.parse(parser, context) - } - - if ((converter.required || hasKwargs) && parsedCount <= 0) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.invalidValue", - - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), - - converter.getErrorString(context), - ) - ), - - "argumentParser.error.invalidValue", + currentArg, + argumentsObj, + parser + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required) { + throw t + } + } + + is CoalescingConverter<*> -> try { + val parsedCount = if (hasKwargs) { + converter.parse(parser, context, kwValue!!) + } else { + converter.parse(parser, context) + } + + if ((converter.required || hasKwargs) && parsedCount <= 0) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.invalidValue", + + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), + + converter.getErrorString(context), + ) + ), + + "argumentParser.error.invalidValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - if (hasKwargs) { - if (parsedCount < kwValue!!.size) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.notAllValid", + if (hasKwargs) { + if (parsedCount < kwValue!!.size) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.notAllValid", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = converter.bundle + ), - kwValue.size, - parsedCount, - context.translate(converter.signatureTypeString, bundleName = converter.bundle) - ) - ), + kwValue.size, + parsedCount, + context.translate(converter.signatureTypeString, bundleName = converter.bundle) + ) + ), - "argumentParser.error.notAllValid", + "argumentParser.error.notAllValid", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - converter.parseSuccess = true + converter.parseSuccess = true - converter.validate(context) - } else { - if (parsedCount > 0) { - logger.trace { "Argument '${currentArg.displayName}' successfully filled." } + converter.validate(context) + } else { + if (parsedCount > 0) { + logger.trace { "Argument '${currentArg.displayName}' successfully filled." } - converter.parseSuccess = true + converter.parseSuccess = true - converter.validate(context) - } - } - } catch (e: DiscordRelayedException) { - if (converter.required) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.errorInArgument", + converter.validate(context) + } + } + } catch (e: DiscordRelayedException) { + if (converter.required) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.errorInArgument", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - converter.handleError(e, context) - ) - ), + converter.handleError(e, context) + ) + ), - "argumentParser.error.errorInArgument", + "argumentParser.error.errorInArgument", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required) { - throw t - } - } - - is OptionalCoalescingConverter<*> -> try { - val parsedCount = if (hasKwargs) { - converter.parse(parser, context, kwValue!!) - } else { - converter.parse(parser, context) - } - - if ((converter.required || hasKwargs) && parsedCount <= 0) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.invalidValue", - - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), - - converter.getErrorString(context), - ) - ), - - "argumentParser.error.invalidValue", + currentArg, + argumentsObj, + parser + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required) { + throw t + } + } + + is OptionalCoalescingConverter<*> -> try { + val parsedCount = if (hasKwargs) { + converter.parse(parser, context, kwValue!!) + } else { + converter.parse(parser, context) + } + + if ((converter.required || hasKwargs) && parsedCount <= 0) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.invalidValue", + + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), + + converter.getErrorString(context), + ) + ), + + "argumentParser.error.invalidValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - if (hasKwargs) { - if (parsedCount < kwValue!!.size) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.notAllValid", + if (hasKwargs) { + if (parsedCount < kwValue!!.size) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.notAllValid", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = converter.bundle + ), - kwValue.size, - parsedCount, - context.translate(converter.signatureTypeString, bundleName = converter.bundle) - ) - ), + kwValue.size, + parsedCount, + context.translate(converter.signatureTypeString, bundleName = converter.bundle) + ) + ), - "argumentParser.error.notAllValid", + "argumentParser.error.notAllValid", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - converter.parseSuccess = true - } else { - if (parsedCount > 0) { - logger.trace { "Argument '${currentArg.displayName}' successfully filled." } + converter.parseSuccess = true + } else { + if (parsedCount > 0) { + logger.trace { "Argument '${currentArg.displayName}' successfully filled." } - converter.parseSuccess = true - } - } + converter.parseSuccess = true + } + } - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required || converter.outputError || hasKwargs) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.errorInArgument", + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required || converter.outputError || hasKwargs) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.errorInArgument", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - converter.handleError(e, context) - ) - ), + converter.handleError(e, context) + ) + ), - "argumentParser.error.errorInArgument", + "argumentParser.error.errorInArgument", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required || converter.outputError || hasKwargs) { - throw t - } - } - - is DefaultingCoalescingConverter<*> -> try { - val parsedCount = if (hasKwargs) { - converter.parse(parser, context, kwValue!!) - } else { - converter.parse(parser, context) - } - - if ((converter.required || hasKwargs) && parsedCount <= 0) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.invalidValue", - - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), - - converter.getErrorString(context), - ) - ), - - "argumentParser.error.invalidValue", + currentArg, + argumentsObj, + parser + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required || converter.outputError || hasKwargs) { + throw t + } + } + + is DefaultingCoalescingConverter<*> -> try { + val parsedCount = if (hasKwargs) { + converter.parse(parser, context, kwValue!!) + } else { + converter.parse(parser, context) + } + + if ((converter.required || hasKwargs) && parsedCount <= 0) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.invalidValue", + + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), + + converter.getErrorString(context), + ) + ), + + "argumentParser.error.invalidValue", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - if (hasKwargs) { - if (parsedCount < kwValue!!.size) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.notAllValid", + if (hasKwargs) { + if (parsedCount < kwValue!!.size) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.notAllValid", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - kwValue.size, - parsedCount, - context.translate(converter.signatureTypeString, bundleName = converter.bundle) - ) - ), + kwValue.size, + parsedCount, + context.translate(converter.signatureTypeString, bundleName = converter.bundle) + ) + ), - "argumentParser.error.notAllValid", + "argumentParser.error.notAllValid", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } + currentArg, + argumentsObj, + parser + ) + } - converter.parseSuccess = true - } else { - if (parsedCount > 0) { - logger.trace { "Argument '${currentArg.displayName}' successfully filled." } + converter.parseSuccess = true + } else { + if (parsedCount > 0) { + logger.trace { "Argument '${currentArg.displayName}' successfully filled." } - converter.parseSuccess = true - } - } + converter.parseSuccess = true + } + } - converter.validate(context) - } catch (e: DiscordRelayedException) { - if (converter.required || converter.outputError || hasKwargs) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.errorInArgument", + converter.validate(context) + } catch (e: DiscordRelayedException) { + if (converter.required || converter.outputError || hasKwargs) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.errorInArgument", - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), - converter.handleError(e, context) - ) - ), + converter.handleError(e, context) + ) + ), - "argumentParser.error.errorInArgument", + "argumentParser.error.errorInArgument", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } - } catch (t: Throwable) { - logger.debug { "Argument ${currentArg.displayName} threw: $t" } - - if (converter.required || converter.outputError) { - throw t - } - } - - else -> throw ArgumentParsingException( - context.translate( - "argumentParser.error.errorInArgument", - - replacements = arrayOf( - context.translate( - currentArg.displayName, - bundleName = context.command.resolvedBundle ?: converter.bundle - ), - - context.translate( - "argumentParser.error.unknownConverterType", - replacements = arrayOf(currentArg.converter) - ) - ) - ), - - "argumentParser.error.errorInArgument", + currentArg, + argumentsObj, + parser + ) + } + } catch (t: Throwable) { + logger.debug { "Argument ${currentArg.displayName} threw: $t" } + + if (converter.required || converter.outputError) { + throw t + } + } + + else -> throw ArgumentParsingException( + context.translate( + "argumentParser.error.errorInArgument", + + replacements = arrayOf( + context.translate( + currentArg.displayName, + bundleName = context.command.resolvedBundle ?: converter.bundle + ), + + context.translate( + "argumentParser.error.unknownConverterType", + replacements = arrayOf(currentArg.converter) + ) + ) + ), + + "argumentParser.error.errorInArgument", context.getLocale(), context.command.resolvedBundle ?: converter.bundle, - currentArg, - argumentsObj, - parser - ) - } - } + currentArg, + argumentsObj, + parser + ) + } + } - val allRequiredArgs = argsMap.count { it.value.converter.required } - val filledRequiredArgs = argsMap.count { it.value.converter.parseSuccess && it.value.converter.required } + val allRequiredArgs = argsMap.count { it.value.converter.required } + val filledRequiredArgs = argsMap.count { it.value.converter.parseSuccess && it.value.converter.required } - logger.trace { "Filled $filledRequiredArgs / $allRequiredArgs arguments." } + logger.trace { "Filled $filledRequiredArgs / $allRequiredArgs arguments." } - if (filledRequiredArgs < allRequiredArgs) { - if (filledRequiredArgs < 1) { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.noFilledArguments", + if (filledRequiredArgs < allRequiredArgs) { + if (filledRequiredArgs < 1) { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.noFilledArguments", - replacements = arrayOf( - allRequiredArgs - ) - ), + replacements = arrayOf( + allRequiredArgs + ) + ), - "argumentParser.error.noFilledArguments", + "argumentParser.error.noFilledArguments", context.getLocale(), context.command.resolvedBundle, - null, - argumentsObj, - parser - ) - } else { - throw ArgumentParsingException( - context.translate( - "argumentParser.error.someFilledArguments", + null, + argumentsObj, + parser + ) + } else { + throw ArgumentParsingException( + context.translate( + "argumentParser.error.someFilledArguments", - replacements = arrayOf( - allRequiredArgs, - filledRequiredArgs - ) - ), + replacements = arrayOf( + allRequiredArgs, + filledRequiredArgs + ) + ), - "argumentParser.error.someFilledArguments", + "argumentParser.error.someFilledArguments", context.getLocale(), context.command.resolvedBundle, - null, - argumentsObj, - parser - ) - } - } - - return argumentsObj - } - - /** - * Generate a command signature based on an [Arguments] subclass. - * - * @param builder Builder returning an [Arguments] subclass - usually the constructor. - * @return MessageCommand arguments signature for display. - */ - public open fun signature(builder: () -> Arguments, locale: Locale): String { - val argumentsObj = builder.invoke() - val parts = mutableListOf() - - argumentsObj.args.forEach { - val signature = buildString { - if (it.converter.required) { - append("<") - } else { - append("[") - } - - append(it.displayName) - - if (it.converter.showTypeInSignature) { - append(": ") - - append( - translationsProvider.translate( - it.converter.signatureTypeString, - it.converter.bundle, - locale - ) - ) - - if (it.converter is DefaultingConverter<*>) { - append("=") - append(it.converter.parsed) - } - } - - if (it.converter is ListConverter<*>) { - append("...") - } - - if (it.converter.required) { - append(">") - } else { - append("]") - } - } - - parts.add(signature) - } - - return parts.joinToString(" ") - } + null, + argumentsObj, + parser + ) + } + } + + return argumentsObj + } + + /** + * Generate a command signature based on an [Arguments] subclass. + * + * @param builder Builder returning an [Arguments] subclass - usually the constructor. + * @return MessageCommand arguments signature for display. + */ + public open fun signature(builder: () -> Arguments, locale: Locale): String { + val argumentsObj = builder.invoke() + val parts = mutableListOf() + + argumentsObj.args.forEach { + val signature = buildString { + if (it.converter.required) { + append("<") + } else { + append("[") + } + + append(it.displayName) + + if (it.converter.showTypeInSignature) { + append(": ") + + append( + translationsProvider.translate( + it.converter.signatureTypeString, + it.converter.bundle, + locale + ) + ) + + if (it.converter is DefaultingConverter<*>) { + append("=") + append(it.converter.parsed) + } + } + + if (it.converter is ListConverter<*>) { + append("...") + } + + if (it.converter.required) { + append(">") + } else { + append("]") + } + } + + parts.add(signature) + } + + return parts.joinToString(" ") + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatGroupCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatGroupCommand.kt index d19589afac..cdcca0573d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatGroupCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatGroupCommand.kt @@ -35,212 +35,212 @@ private val logger = KotlinLogging.logger {} // This is intentional @ExtensionDSL public open class ChatGroupCommand( - extension: Extension, - arguments: (() -> T)? = null, - public open val parent: ChatGroupCommand? = null + extension: Extension, + arguments: (() -> T)? = null, + public open val parent: ChatGroupCommand? = null, ) : ChatCommand(extension, arguments) { - /** @suppress **/ - public val botSettings: ExtensibleBotBuilder by inject() - - /** @suppress **/ - public open val commands: MutableList> = mutableListOf() - - override lateinit var name: String - - /** @suppress **/ - override var body: suspend ChatCommandContext.() -> Unit = { - sendHelp() - } - - override suspend fun runChecks( - event: MessageCreateEvent, - sendMessage: Boolean, - cache: MutableStringKeyedMap - ): Boolean { - var result = parent?.runChecks(event, sendMessage, cache) ?: true - - if (result) { - result = super.runChecks(event, sendMessage, cache) - } - - return result - } - - /** - * An internal function used to ensure that all of a command group's required arguments are present. - * - * @throws InvalidCommandException Thrown when a required argument hasn't been set. - */ - @Throws(InvalidCommandException::class) - override fun validate() { - if (!::name.isInitialized) { - throw InvalidCommandException(null, "No command name given.") - } - - if (commands.isEmpty()) { - throw InvalidCommandException(name, "No subcommands registered.") - } - } - - /** - * DSL function for easily registering a command. - * - * Use this in your setup function to register a command that may be executed on Discord. - * - * @param body Builder lambda used for setting up the command object. - */ - @ExtensionDSL - public open suspend fun chatCommand( - arguments: (() -> R)?, - body: suspend ChatCommand.() -> Unit - ): ChatCommand { - val commandObj = ChatSubCommand(extension, arguments, this) - body.invoke(commandObj) - - return chatCommand(commandObj) - } - - /** - * DSL function for easily registering a command, without arguments. - * - * Use this in your setup function to register a command that may be executed on Discord. - * - * @param body Builder lambda used for setting up the command object. - */ - @ExtensionDSL - public open suspend fun chatCommand( - body: suspend ChatCommand.() -> Unit - ): ChatCommand { - val commandObj = ChatSubCommand(extension, parent = this) - body.invoke(commandObj) - - return chatCommand(commandObj) - } - - /** - * Function for registering a custom command object. - * - * You can use this if you have a custom command subclass you need to register. - * - * @param commandObj MessageCommand object to register. - */ - @ExtensionDSL - public open suspend fun chatCommand( - commandObj: ChatCommand - ): ChatCommand { - try { - commandObj.validate() - commands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register subcommand - $e" } - } - - return commandObj - } - - /** - * DSL function for easily registering a grouped command. - * - * Use this in your setup function to register a group of commands. - * - * The body of the grouped command will be executed if there is no - * matching subcommand. - * - * @param body Builder lambda used for setting up the command object. - */ - @ExtensionDSL - @Suppress("MemberNameEqualsClassName") // Really? - public open suspend fun chatGroupCommand( - arguments: (() -> R)?, - body: suspend ChatGroupCommand.() -> Unit - ): ChatGroupCommand { - val commandObj = ChatGroupCommand(extension, arguments, this) - body.invoke(commandObj) - - return chatCommand(commandObj) as ChatGroupCommand - } - - /** - * DSL function for easily registering a grouped command, without its own arguments. - * - * Use this in your setup function to register a group of commands. - * - * The body of the grouped command will be executed if there is no - * matching subcommand. - * - * @param body Builder lambda used for setting up the command object. - */ - @ExtensionDSL - @Suppress("MemberNameEqualsClassName") // Really? - public open suspend fun chatGroupCommand( - body: suspend ChatGroupCommand.() -> Unit - ): ChatGroupCommand { - val commandObj = ChatGroupCommand(extension, parent = this) - body.invoke(commandObj) - - return chatCommand(commandObj) as ChatGroupCommand - } - - /** @suppress **/ - public open suspend fun getCommand( - name: String?, - event: MessageCreateEvent - ): ChatCommand? { - name ?: return null - - val defaultLocale = botSettings.i18nBuilder.defaultLocale - val locale = event.getLocale() - - return commands.firstOrNull { it.getTranslatedName(locale) == name } - ?: commands.firstOrNull { it.getTranslatedAliases(locale).contains(name) } - ?: commands.firstOrNull { it.localeFallback && it.getTranslatedName(defaultLocale) == name } - ?: commands.firstOrNull { it.localeFallback && it.getTranslatedAliases(defaultLocale).contains(name) } - } - - /** - * Execute this grouped command, given a [MessageCreateEvent]. - * - * This function takes a [MessageCreateEvent] (generated when a message is received), and - * processes it. The command's checks are invoked and, assuming all of the - * checks passed, the command will search for a subcommand matching the first argument. - * If a subcommand is found, it will be executed - otherwise, the the - * [command body][action] is executed. - * - * If an exception is thrown by the [command body][action], it is caught and a traceback - * is printed. - * - * @param event The message creation event. - */ - override suspend fun call( - event: MessageCreateEvent, - commandName: String, - parser: StringParser, - argString: String, - skipChecks: Boolean, - cache: MutableStringKeyedMap - ) { - if (skipChecks || !runChecks(event, cache = cache)) { - return - } - - val command = parser.peekNext()?.data?.lowercase() - val subCommand = getCommand(command, event) - - if (subCommand == null) { - super.call(event, commandName, parser, argString, true, cache) - } else { - parser.parseNext() // Advance the cursor so proper parsing can happen - - subCommand.call(event, commandName, StringParser(parser.consumeRemaining()), argString) - } - } - - /** Get the full command name, translated, with parent commands taken into account. **/ - public open suspend fun getFullTranslatedName(locale: Locale): String { - parent ?: return this.getTranslatedName(locale) - - return parent!!.getFullTranslatedName(locale) + " " + this.getTranslatedName(locale) - } + /** @suppress **/ + public val botSettings: ExtensibleBotBuilder by inject() + + /** @suppress **/ + public open val commands: MutableList> = mutableListOf() + + override lateinit var name: String + + /** @suppress **/ + override var body: suspend ChatCommandContext.() -> Unit = { + sendHelp() + } + + override suspend fun runChecks( + event: MessageCreateEvent, + sendMessage: Boolean, + cache: MutableStringKeyedMap, + ): Boolean { + var result = parent?.runChecks(event, sendMessage, cache) ?: true + + if (result) { + result = super.runChecks(event, sendMessage, cache) + } + + return result + } + + /** + * An internal function used to ensure that all of a command group's required arguments are present. + * + * @throws InvalidCommandException Thrown when a required argument hasn't been set. + */ + @Throws(InvalidCommandException::class) + override fun validate() { + if (!::name.isInitialized) { + throw InvalidCommandException(null, "No command name given.") + } + + if (commands.isEmpty()) { + throw InvalidCommandException(name, "No subcommands registered.") + } + } + + /** + * DSL function for easily registering a command. + * + * Use this in your setup function to register a command that may be executed on Discord. + * + * @param body Builder lambda used for setting up the command object. + */ + @ExtensionDSL + public open suspend fun chatCommand( + arguments: (() -> R)?, + body: suspend ChatCommand.() -> Unit, + ): ChatCommand { + val commandObj = ChatSubCommand(extension, arguments, this) + body.invoke(commandObj) + + return chatCommand(commandObj) + } + + /** + * DSL function for easily registering a command, without arguments. + * + * Use this in your setup function to register a command that may be executed on Discord. + * + * @param body Builder lambda used for setting up the command object. + */ + @ExtensionDSL + public open suspend fun chatCommand( + body: suspend ChatCommand.() -> Unit, + ): ChatCommand { + val commandObj = ChatSubCommand(extension, parent = this) + body.invoke(commandObj) + + return chatCommand(commandObj) + } + + /** + * Function for registering a custom command object. + * + * You can use this if you have a custom command subclass you need to register. + * + * @param commandObj MessageCommand object to register. + */ + @ExtensionDSL + public open suspend fun chatCommand( + commandObj: ChatCommand, + ): ChatCommand { + try { + commandObj.validate() + commands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register subcommand - $e" } + } + + return commandObj + } + + /** + * DSL function for easily registering a grouped command. + * + * Use this in your setup function to register a group of commands. + * + * The body of the grouped command will be executed if there is no + * matching subcommand. + * + * @param body Builder lambda used for setting up the command object. + */ + @ExtensionDSL + @Suppress("MemberNameEqualsClassName") // Really? + public open suspend fun chatGroupCommand( + arguments: (() -> R)?, + body: suspend ChatGroupCommand.() -> Unit, + ): ChatGroupCommand { + val commandObj = ChatGroupCommand(extension, arguments, this) + body.invoke(commandObj) + + return chatCommand(commandObj) as ChatGroupCommand + } + + /** + * DSL function for easily registering a grouped command, without its own arguments. + * + * Use this in your setup function to register a group of commands. + * + * The body of the grouped command will be executed if there is no + * matching subcommand. + * + * @param body Builder lambda used for setting up the command object. + */ + @ExtensionDSL + @Suppress("MemberNameEqualsClassName") // Really? + public open suspend fun chatGroupCommand( + body: suspend ChatGroupCommand.() -> Unit, + ): ChatGroupCommand { + val commandObj = ChatGroupCommand(extension, parent = this) + body.invoke(commandObj) + + return chatCommand(commandObj) as ChatGroupCommand + } + + /** @suppress **/ + public open suspend fun getCommand( + name: String?, + event: MessageCreateEvent, + ): ChatCommand? { + name ?: return null + + val defaultLocale = botSettings.i18nBuilder.defaultLocale + val locale = event.getLocale() + + return commands.firstOrNull { it.getTranslatedName(locale) == name } + ?: commands.firstOrNull { it.getTranslatedAliases(locale).contains(name) } + ?: commands.firstOrNull { it.localeFallback && it.getTranslatedName(defaultLocale) == name } + ?: commands.firstOrNull { it.localeFallback && it.getTranslatedAliases(defaultLocale).contains(name) } + } + + /** + * Execute this grouped command, given a [MessageCreateEvent]. + * + * This function takes a [MessageCreateEvent] (generated when a message is received), and + * processes it. The command's checks are invoked and, assuming all of the + * checks passed, the command will search for a subcommand matching the first argument. + * If a subcommand is found, it will be executed - otherwise, the the + * [command body][action] is executed. + * + * If an exception is thrown by the [command body][action], it is caught and a traceback + * is printed. + * + * @param event The message creation event. + */ + override suspend fun call( + event: MessageCreateEvent, + commandName: String, + parser: StringParser, + argString: String, + skipChecks: Boolean, + cache: MutableStringKeyedMap, + ) { + if (skipChecks || !runChecks(event, cache = cache)) { + return + } + + val command = parser.peekNext()?.data?.lowercase() + val subCommand = getCommand(command, event) + + if (subCommand == null) { + super.call(event, commandName, parser, argString, true, cache) + } else { + parser.parseNext() // Advance the cursor so proper parsing can happen + + subCommand.call(event, commandName, StringParser(parser.consumeRemaining()), argString) + } + } + + /** Get the full command name, translated, with parent commands taken into account. **/ + public open suspend fun getFullTranslatedName(locale: Locale): String { + parent ?: return this.getTranslatedName(locale) + + return parent!!.getFullTranslatedName(locale) + " " + this.getTranslatedName(locale) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatSubCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatSubCommand.kt index c83ad93b73..43d63e569a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatSubCommand.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/chat/ChatSubCommand.kt @@ -23,32 +23,32 @@ import java.util.* */ @ExtensionDSL public open class ChatSubCommand( - extension: Extension, - arguments: (() -> T)? = null, - public open val parent: ChatGroupCommand, + extension: Extension, + arguments: (() -> T)? = null, + public open val parent: ChatGroupCommand, ) : ChatCommand(extension, arguments) { - override suspend fun runChecks( - event: MessageCreateEvent, - sendMessage: Boolean, - cache: MutableStringKeyedMap, - ): Boolean = - parent.runChecks(event, sendMessage, cache) && - super.runChecks(event, sendMessage, cache) + override suspend fun runChecks( + event: MessageCreateEvent, + sendMessage: Boolean, + cache: MutableStringKeyedMap, + ): Boolean = + parent.runChecks(event, sendMessage, cache) && + super.runChecks(event, sendMessage, cache) - /** Get the full command name, translated, with parent commands taken into account. **/ - public open suspend fun getFullTranslatedName(locale: Locale): String = - parent.getFullTranslatedName(locale) + " " + this.getTranslatedName(locale) + /** Get the full command name, translated, with parent commands taken into account. **/ + public open suspend fun getFullTranslatedName(locale: Locale): String = + parent.getFullTranslatedName(locale) + " " + this.getTranslatedName(locale) - override fun getTranslatedName(locale: Locale): String { - if (!nameTranslationCache.containsKey(locale)) { - nameTranslationCache[locale] = translationsProvider.translate( - this.name, - this.resolvedBundle, - locale - ).lowercase() - } + override fun getTranslatedName(locale: Locale): String { + if (!nameTranslationCache.containsKey(locale)) { + nameTranslationCache[locale] = translationsProvider.translate( + this.name, + this.resolvedBundle, + locale + ).lowercase() + } - return nameTranslationCache[locale]!! - } + return nameTranslationCache[locale]!! + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/Annotations.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/Annotations.kt index 515e9adb9c..561ef14740 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/Annotations.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/Annotations.kt @@ -7,13 +7,13 @@ package com.kotlindiscord.kord.extensions.commands.converters @RequiresOptIn( - message = "When creating an Arguments class, you must use one of the converter functions starting with " + - "`defaulting` instead. Otherwise, if you know what you're doing (or you're writing your own converter " + - "functions), please feel free to opt-in.\n\n" + + message = "When creating an Arguments class, you must use one of the converter functions starting with " + + "`defaulting` instead. Otherwise, if you know what you're doing (or you're writing your own converter " + + "functions), please feel free to opt-in.\n\n" + - "For example, instead of `boolean(...).toDefaulting()`, consider `defaultingBoolean(...)`.\n\n" + + "For example, instead of `boolean(...).toDefaulting()`, consider `defaultingBoolean(...)`.\n\n" + - "Failure to register your converters this way may result in strange or broken behaviour." + "Failure to register your converters this way may result in strange or broken behaviour." ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.FUNCTION) @@ -21,13 +21,13 @@ package com.kotlindiscord.kord.extensions.commands.converters public annotation class ConverterToDefaulting @RequiresOptIn( - message = "When creating an Arguments class, you must use one of the converter functions ending with " + - "\"List\" instead. Otherwise, if you know what you're doing (or you're writing your own converter " + - "functions), please feel free to opt-in.\n\n" + + message = "When creating an Arguments class, you must use one of the converter functions ending with " + + "\"List\" instead. Otherwise, if you know what you're doing (or you're writing your own converter " + + "functions), please feel free to opt-in.\n\n" + - "For example, instead of `boolean(...).toMulti()`, consider `booleanList(...)`.\n\n" + + "For example, instead of `boolean(...).toMulti()`, consider `booleanList(...)`.\n\n" + - "Failure to register your converters this way may result in strange or broken behaviour." + "Failure to register your converters this way may result in strange or broken behaviour." ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.FUNCTION) @@ -35,13 +35,13 @@ public annotation class ConverterToDefaulting public annotation class ConverterToMulti @RequiresOptIn( - message = "When creating an Arguments class, you must use one of the converter functions starting with " + - "\"optional\" instead. Otherwise, if you know what you're doing (or you're writing your own converter " + - "functions), please feel free to opt-in.\n\n" + + message = "When creating an Arguments class, you must use one of the converter functions starting with " + + "\"optional\" instead. Otherwise, if you know what you're doing (or you're writing your own converter " + + "functions), please feel free to opt-in.\n\n" + - "For example, instead of `boolean(...).toOptional()`, consider `optionalBoolean(...)`.\n\n" + + "For example, instead of `boolean(...).toOptional()`, consider `optionalBoolean(...)`.\n\n" + - "Failure to register your converters this way may result in strange or broken behaviour." + "Failure to register your converters this way may result in strange or broken behaviour." ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.FUNCTION) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingConverter.kt index d90381fdb8..0dfa2cb7ae 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingConverter.kt @@ -29,102 +29,102 @@ import com.kotlindiscord.kord.extensions.commands.converters.builders.Coalescing * @property validator Validation lambda, which may throw a [DiscordRelayedException] if required. */ public abstract class CoalescingConverter( - public open val shouldThrow: Boolean = false, - override var validator: Validator = null + public open val shouldThrow: Boolean = false, + override var validator: Validator = null, ) : Converter, T, List, Int>(true), SlashCommandConverter { - /** - * The parsed value. - * - * This should be set by the converter during the course of the [parse] function. - */ - public override lateinit var parsed: T + /** + * The parsed value. + * + * This should be set by the converter during the course of the [parse] function. + */ + public override lateinit var parsed: T - /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ - public open lateinit var builder: CoalescingConverterBuilder + /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ + public open lateinit var builder: CoalescingConverterBuilder - /** @suppress Internal function used by converter builders. **/ - public open fun withBuilder( - builder: CoalescingConverterBuilder - ): CoalescingConverter { - this.builder = builder - this.genericBuilder = builder + /** @suppress Internal function used by converter builders. **/ + public open fun withBuilder( + builder: CoalescingConverterBuilder, + ): CoalescingConverter { + this.builder = builder + this.genericBuilder = builder - return this - } + return this + } - /** - * Wrap this coalescing converter with a [CoalescingToOptionalConverter], which is a special converter that will - * act like an [OptionalCoalescingConverter] using the same logic of this converter. - * - * Your converter should be designed with this pattern in mind. If that's not possible, please override this - * function and throw an exception in the body. - * - * For more information on the parameters, see [Converter]. - * - * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter - * provides. - * - * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a - * generated command signature. - * - * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter - * provides. - * - * @param outputError Optionally, provide `true` to fail parsing and return errors if the converter throws a - * [DiscordRelayedException], instead of continuing. You probably only want to set this if the converter is the - * last one in a set of arguments. - */ - @ConverterToOptional - public open fun toOptional( - signatureTypeString: String? = null, - showTypeInSignature: Boolean? = null, - errorTypeString: String? = null, - outputError: Boolean = false, - nestedValidator: Validator = null - ): OptionalCoalescingConverter = CoalescingToOptionalConverter( - this, - signatureTypeString, - showTypeInSignature, - errorTypeString, - outputError, - nestedValidator - ) + /** + * Wrap this coalescing converter with a [CoalescingToOptionalConverter], which is a special converter that will + * act like an [OptionalCoalescingConverter] using the same logic of this converter. + * + * Your converter should be designed with this pattern in mind. If that's not possible, please override this + * function and throw an exception in the body. + * + * For more information on the parameters, see [Converter]. + * + * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter + * provides. + * + * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a + * generated command signature. + * + * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter + * provides. + * + * @param outputError Optionally, provide `true` to fail parsing and return errors if the converter throws a + * [DiscordRelayedException], instead of continuing. You probably only want to set this if the converter is the + * last one in a set of arguments. + */ + @ConverterToOptional + public open fun toOptional( + signatureTypeString: String? = null, + showTypeInSignature: Boolean? = null, + errorTypeString: String? = null, + outputError: Boolean = false, + nestedValidator: Validator = null, + ): OptionalCoalescingConverter = CoalescingToOptionalConverter( + this, + signatureTypeString, + showTypeInSignature, + errorTypeString, + outputError, + nestedValidator + ) - /** - * Wrap this coalescing converter with a [CoalescingToDefaultingConverter], which is a special converter that will - * act like an [DefaultingCoalescingConverter] using the same logic of this converter. - * - * Your converter should be designed with this pattern in mind. If that's not possible, please override this - * function and throw an exception in the body. - * - * For more information on the parameters, see [Converter]. - * - * @param defaultValue The default value to use when an argument can't be converted. - * @param outputError Whether the argument parser should output parsing errors on invalid arguments. - * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter - * provides. - * - * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a - * generated command signature. - * - * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter - * provides. - */ - @ConverterToDefaulting - public open fun toDefaulting( - defaultValue: T, - outputError: Boolean = false, - signatureTypeString: String? = null, - showTypeInSignature: Boolean? = null, - errorTypeString: String? = null, - nestedValidator: Validator = null - ): DefaultingCoalescingConverter = CoalescingToDefaultingConverter( - this, - defaultValue = defaultValue, - outputError = outputError, - newSignatureTypeString = signatureTypeString, - newShowTypeInSignature = showTypeInSignature, - newErrorTypeString = errorTypeString, - validator = nestedValidator, - ) + /** + * Wrap this coalescing converter with a [CoalescingToDefaultingConverter], which is a special converter that will + * act like an [DefaultingCoalescingConverter] using the same logic of this converter. + * + * Your converter should be designed with this pattern in mind. If that's not possible, please override this + * function and throw an exception in the body. + * + * For more information on the parameters, see [Converter]. + * + * @param defaultValue The default value to use when an argument can't be converted. + * @param outputError Whether the argument parser should output parsing errors on invalid arguments. + * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter + * provides. + * + * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a + * generated command signature. + * + * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter + * provides. + */ + @ConverterToDefaulting + public open fun toDefaulting( + defaultValue: T, + outputError: Boolean = false, + signatureTypeString: String? = null, + showTypeInSignature: Boolean? = null, + errorTypeString: String? = null, + nestedValidator: Validator = null, + ): DefaultingCoalescingConverter = CoalescingToDefaultingConverter( + this, + defaultValue = defaultValue, + outputError = outputError, + newSignatureTypeString = signatureTypeString, + newShowTypeInSignature = showTypeInSignature, + newErrorTypeString = errorTypeString, + validator = nestedValidator, + ) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingToDefaultingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingToDefaultingConverter.kt index 3671a98819..2bba65ea8f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingToDefaultingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingToDefaultingConverter.kt @@ -27,51 +27,51 @@ import dev.kord.rest.builder.interaction.OptionsBuilder * @param newErrorTypeString An optional error type string to override the one set in [coalescingConverter]. */ public class CoalescingToDefaultingConverter( - public val coalescingConverter: CoalescingConverter, - defaultValue: T, - outputError: Boolean = false, + public val coalescingConverter: CoalescingConverter, + defaultValue: T, + outputError: Boolean = false, - newSignatureTypeString: String? = null, - newShowTypeInSignature: Boolean? = null, - newErrorTypeString: String? = null, + newSignatureTypeString: String? = null, + newShowTypeInSignature: Boolean? = null, + newErrorTypeString: String? = null, - override var validator: Validator = null + override var validator: Validator = null, ) : DefaultingCoalescingConverter(defaultValue, outputError = outputError) { - override val signatureTypeString: String = newSignatureTypeString ?: coalescingConverter.signatureTypeString - override val showTypeInSignature: Boolean = newShowTypeInSignature ?: coalescingConverter.showTypeInSignature - override val errorTypeString: String? = newErrorTypeString ?: coalescingConverter.errorTypeString + override val signatureTypeString: String = newSignatureTypeString ?: coalescingConverter.signatureTypeString + override val showTypeInSignature: Boolean = newShowTypeInSignature ?: coalescingConverter.showTypeInSignature + override val errorTypeString: String? = newErrorTypeString ?: coalescingConverter.errorTypeString private val dummyArgs = Arguments() - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - val result = coalescingConverter.parse(parser, context, named) + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + val result = coalescingConverter.parse(parser, context, named) - if (result > 0) { - this.parsed = coalescingConverter.getValue(dummyArgs, coalescingConverter::parsed) - } + if (result > 0) { + this.parsed = coalescingConverter.getValue(dummyArgs, coalescingConverter::parsed) + } - return result - } + return result + } - override suspend fun handleError( - t: Throwable, - context: CommandContext - ): String = coalescingConverter.handleError(t, context) + override suspend fun handleError( + t: Throwable, + context: CommandContext, + ): String = coalescingConverter.handleError(t, context) - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder { - val option = coalescingConverter.toSlashOption(arg) - option.required = false + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder { + val option = coalescingConverter.toSlashOption(arg) + option.required = false - return option - } + return option + } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val result = coalescingConverter.parseOption(context, option) + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val result = coalescingConverter.parseOption(context, option) - if (result) { - this.parsed = coalescingConverter.getValue(dummyArgs, coalescingConverter::parsed) - } + if (result) { + this.parsed = coalescingConverter.getValue(dummyArgs, coalescingConverter::parsed) + } - return result - } + return result + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingToOptionalConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingToOptionalConverter.kt index 5fa4507c69..fd37b1e1f1 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingToOptionalConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/CoalescingToOptionalConverter.kt @@ -27,50 +27,50 @@ import dev.kord.rest.builder.interaction.OptionsBuilder * @param newErrorTypeString An optional error type string to override the one set in [coalescingConverter]. */ public class CoalescingToOptionalConverter( - public val coalescingConverter: CoalescingConverter, + public val coalescingConverter: CoalescingConverter, - newSignatureTypeString: String? = null, - newShowTypeInSignature: Boolean? = null, - newErrorTypeString: String? = null, - outputError: Boolean = false, + newSignatureTypeString: String? = null, + newShowTypeInSignature: Boolean? = null, + newErrorTypeString: String? = null, + outputError: Boolean = false, - override var validator: Validator = null + override var validator: Validator = null, ) : OptionalCoalescingConverter(outputError) { - override val signatureTypeString: String = newSignatureTypeString ?: coalescingConverter.signatureTypeString - override val showTypeInSignature: Boolean = newShowTypeInSignature ?: coalescingConverter.showTypeInSignature - override val errorTypeString: String? = newErrorTypeString ?: coalescingConverter.errorTypeString + override val signatureTypeString: String = newSignatureTypeString ?: coalescingConverter.signatureTypeString + override val showTypeInSignature: Boolean = newShowTypeInSignature ?: coalescingConverter.showTypeInSignature + override val errorTypeString: String? = newErrorTypeString ?: coalescingConverter.errorTypeString private val dummyArgs = Arguments() - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - val result = coalescingConverter.parse(parser, context, named) + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + val result = coalescingConverter.parse(parser, context, named) - if (result > 0) { - this.parsed = coalescingConverter.getValue(dummyArgs, coalescingConverter::parsed) - } + if (result > 0) { + this.parsed = coalescingConverter.getValue(dummyArgs, coalescingConverter::parsed) + } - return result - } + return result + } - override suspend fun handleError( - t: Throwable, - context: CommandContext - ): String = coalescingConverter.handleError(t, context) + override suspend fun handleError( + t: Throwable, + context: CommandContext, + ): String = coalescingConverter.handleError(t, context) - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder { - val option = coalescingConverter.toSlashOption(arg) - option.required = false + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder { + val option = coalescingConverter.toSlashOption(arg) + option.required = false - return option - } + return option + } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val result = coalescingConverter.parseOption(context, option) + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val result = coalescingConverter.parseOption(context, option) - if (result) { - this.parsed = coalescingConverter.getValue(dummyArgs, coalescingConverter::parsed) - } + if (result) { + this.parsed = coalescingConverter.getValue(dummyArgs, coalescingConverter::parsed) + } - return result - } + return result + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/Converter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/Converter.kt index e58fff1fca..646418a5cd 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/Converter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/Converter.kt @@ -37,114 +37,114 @@ import kotlin.reflect.KProperty * @param ResultType TypeVar representing how this converter signals whether it succeeded - either `Boolean` or `Int` */ public abstract class Converter( - public open val required: Boolean = true, + public open val required: Boolean = true, ) : KordExKoinComponent { - /** This is pretty hacky, but there aren't many better options. **/ - internal lateinit var genericBuilder: ConverterBuilder - - /** Current instance of the bot. **/ - public open val bot: ExtensibleBot by inject() - - /** Kord instance, backing the ExtensibleBot. **/ - public val kord: Kord by inject() - - /** - * The parsed value. - * - * This should be set by the converter during the course of the [parse] function. - */ - public abstract var parsed: OutputType - - /** Validation lambda, which may throw a [DiscordRelayedException] if required. **/ - public open var validator: Validator = null - - /** This will be set to true by the argument parser if the conversion succeeded. **/ - public var parseSuccess: Boolean = false - - /** For commands with generated signatures, set whether the type string should be shown in the signature. **/ - public open val showTypeInSignature: Boolean = true - - /** - * Translation key pointing to a short string describing the type of data this converter handles. Should be very - * short. - */ - public abstract val signatureTypeString: String - - /** - * String referring to the translation bundle name required to resolve translations for this converter. - * - * For more information, see the i18n page of the documentation. - */ - public open val bundle: String? = null - - /** - * If the [signatureTypeString] isn't sufficient, you can optionally provide a translation key pointing to a - * longer type string to use for error messages. - */ - public open val errorTypeString: String? = null - - /** Argument object containing this converter and its metadata. **/ - public open lateinit var argumentObj: Argument<*> - - /** For delegation, retrieve the parsed value if it's been set, or null if it hasn't. **/ - public operator fun getValue(thisRef: Arguments, property: KProperty<*>): OutputType = - if (::genericBuilder.isInitialized && genericBuilder.mutator != null) { - genericBuilder.mutator!!(parsed) - } else { - parsed - } - - /** - * Given a Throwable encountered during the [parse] function, return a human-readable string to display on Discord. - * - * For multi converters, this is only called when the converter is required. The default behaviour simply - * re-throws the Throwable (or returns the reason if it's a DiscordRelayedException), so you only need to override - * this if you want to do something else. - */ - public open suspend fun handleError( - t: Throwable, - context: CommandContext - ): String = if (t is DiscordRelayedException) t.reason else throw t - - /** Call the validator lambda, if one was provided. **/ - public open suspend fun validate(context: CommandContext) { - validator?.let { actualValidator -> - val validationContext = ValidationContext(parsed, context) - - actualValidator.invoke(validationContext) - - validationContext.throwIfFailed() - } - } - - /** - * Process the string in the given [parser], converting it into a new value. - * - * The resulting value should be stored in [parsed] - this will not be done for you. - * - * If you'd like to return more detailed feedback to the user on invalid input, you can throw a - * [DiscordRelayedException] here. - * - * @param parser [StringParser] used to parse the command, if any - * @param context MessageCommand context object, containing the event, message, and other command-related things - * - * @return Whether you managed to convert the argument. If you don't want to provide extra context to the user, - * simply return `false` or `0` depending on your converter type - the command system will generate an error - * message for you. - * - * @see Converter - */ - public abstract suspend fun parse( - parser: StringParser?, - context: CommandContext, - named: NamedInputType? = null - ): ResultType - - /** - * Return a translated, formatted error string. - * - * This will attempt to use the [errorTypeString], falling back to [signatureTypeString]. - */ - public open suspend fun getErrorString(context: CommandContext): String = + /** This is pretty hacky, but there aren't many better options. **/ + internal lateinit var genericBuilder: ConverterBuilder + + /** Current instance of the bot. **/ + public open val bot: ExtensibleBot by inject() + + /** Kord instance, backing the ExtensibleBot. **/ + public val kord: Kord by inject() + + /** + * The parsed value. + * + * This should be set by the converter during the course of the [parse] function. + */ + public abstract var parsed: OutputType + + /** Validation lambda, which may throw a [DiscordRelayedException] if required. **/ + public open var validator: Validator = null + + /** This will be set to true by the argument parser if the conversion succeeded. **/ + public var parseSuccess: Boolean = false + + /** For commands with generated signatures, set whether the type string should be shown in the signature. **/ + public open val showTypeInSignature: Boolean = true + + /** + * Translation key pointing to a short string describing the type of data this converter handles. Should be very + * short. + */ + public abstract val signatureTypeString: String + + /** + * String referring to the translation bundle name required to resolve translations for this converter. + * + * For more information, see the i18n page of the documentation. + */ + public open val bundle: String? = null + + /** + * If the [signatureTypeString] isn't sufficient, you can optionally provide a translation key pointing to a + * longer type string to use for error messages. + */ + public open val errorTypeString: String? = null + + /** Argument object containing this converter and its metadata. **/ + public open lateinit var argumentObj: Argument<*> + + /** For delegation, retrieve the parsed value if it's been set, or null if it hasn't. **/ + public operator fun getValue(thisRef: Arguments, property: KProperty<*>): OutputType = + if (::genericBuilder.isInitialized && genericBuilder.mutator != null) { + genericBuilder.mutator!!(parsed) + } else { + parsed + } + + /** + * Given a Throwable encountered during the [parse] function, return a human-readable string to display on Discord. + * + * For multi converters, this is only called when the converter is required. The default behaviour simply + * re-throws the Throwable (or returns the reason if it's a DiscordRelayedException), so you only need to override + * this if you want to do something else. + */ + public open suspend fun handleError( + t: Throwable, + context: CommandContext, + ): String = if (t is DiscordRelayedException) t.reason else throw t + + /** Call the validator lambda, if one was provided. **/ + public open suspend fun validate(context: CommandContext) { + validator?.let { actualValidator -> + val validationContext = ValidationContext(parsed, context) + + actualValidator.invoke(validationContext) + + validationContext.throwIfFailed() + } + } + + /** + * Process the string in the given [parser], converting it into a new value. + * + * The resulting value should be stored in [parsed] - this will not be done for you. + * + * If you'd like to return more detailed feedback to the user on invalid input, you can throw a + * [DiscordRelayedException] here. + * + * @param parser [StringParser] used to parse the command, if any + * @param context MessageCommand context object, containing the event, message, and other command-related things + * + * @return Whether you managed to convert the argument. If you don't want to provide extra context to the user, + * simply return `false` or `0` depending on your converter type - the command system will generate an error + * message for you. + * + * @see Converter + */ + public abstract suspend fun parse( + parser: StringParser?, + context: CommandContext, + named: NamedInputType? = null, + ): ResultType + + /** + * Return a translated, formatted error string. + * + * This will attempt to use the [errorTypeString], falling back to [signatureTypeString]. + */ + public open suspend fun getErrorString(context: CommandContext): String = context.translate(errorTypeString ?: signatureTypeString) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/DefaultingCoalescingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/DefaultingCoalescingConverter.kt index b781fdfe56..2688187ae5 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/DefaultingCoalescingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/DefaultingCoalescingConverter.kt @@ -24,27 +24,27 @@ import com.kotlindiscord.kord.extensions.commands.converters.builders.Defaulting * @property validator Validation lambda, which may throw a DiscordRelayedException if required. */ public abstract class DefaultingCoalescingConverter( - defaultValue: T, - public val outputError: Boolean = false, - override var validator: Validator = null + defaultValue: T, + public val outputError: Boolean = false, + override var validator: Validator = null, ) : Converter, T, List, Int>(false), SlashCommandConverter { - /** - * The parsed value. - * - * This should be set by the converter during the course of the [parse] function. - */ - public override var parsed: T = defaultValue + /** + * The parsed value. + * + * This should be set by the converter during the course of the [parse] function. + */ + public override var parsed: T = defaultValue - /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ - public open lateinit var builder: DefaultingCoalescingConverterBuilder + /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ + public open lateinit var builder: DefaultingCoalescingConverterBuilder - /** @suppress Internal function used by converter builders. **/ - public open fun withBuilder( - builder: DefaultingCoalescingConverterBuilder - ): DefaultingCoalescingConverter { - this.builder = builder - this.genericBuilder = builder + /** @suppress Internal function used by converter builders. **/ + public open fun withBuilder( + builder: DefaultingCoalescingConverterBuilder, + ): DefaultingCoalescingConverter { + this.builder = builder + this.genericBuilder = builder - return this - } + return this + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/DefaultingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/DefaultingConverter.kt index a436e84215..68a3356547 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/DefaultingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/DefaultingConverter.kt @@ -20,27 +20,27 @@ import com.kotlindiscord.kord.extensions.commands.converters.builders.Defaulting * @property validator Validation lambda, which may throw a DiscordRelayedException if required. */ public abstract class DefaultingConverter( - defaultValue: T, - public val outputError: Boolean = false, - override var validator: Validator = null + defaultValue: T, + public val outputError: Boolean = false, + override var validator: Validator = null, ) : Converter(false), SlashCommandConverter { - /** - * The parsed value. - * - * This should be set by the converter during the course of the [parse] function. - */ - public override var parsed: T = defaultValue + /** + * The parsed value. + * + * This should be set by the converter during the course of the [parse] function. + */ + public override var parsed: T = defaultValue - /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ - public open lateinit var builder: DefaultingConverterBuilder + /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ + public open lateinit var builder: DefaultingConverterBuilder - /** @suppress Internal function used by converter builders. **/ - public open fun withBuilder( - builder: DefaultingConverterBuilder - ): DefaultingConverter { - this.builder = builder - this.genericBuilder = builder + /** @suppress Internal function used by converter builders. **/ + public open fun withBuilder( + builder: DefaultingConverterBuilder, + ): DefaultingConverter { + this.builder = builder + this.genericBuilder = builder - return this - } + return this + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/ListConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/ListConverter.kt index 5cd16866e3..25b555e5ee 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/ListConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/ListConverter.kt @@ -21,26 +21,26 @@ import com.kotlindiscord.kord.extensions.commands.converters.builders.ListConver * @property validator Validation lambda, which may throw a DiscordRelayedException if required. */ public abstract class ListConverter( - required: Boolean = true, - override var validator: Validator> = null + required: Boolean = true, + override var validator: Validator> = null, ) : Converter, List, List, Int>(required), SlashCommandConverter { - /** - * The parsed value. - * - * This should be set by the converter during the course of the [parse] function. - */ - public override var parsed: List = listOf() + /** + * The parsed value. + * + * This should be set by the converter during the course of the [parse] function. + */ + public override var parsed: List = listOf() - /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ - public open lateinit var builder: ListConverterBuilder + /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ + public open lateinit var builder: ListConverterBuilder - /** @suppress Internal function used by converter builders. **/ - public open fun withBuilder( - builder: ListConverterBuilder - ): ListConverter { - this.builder = builder - this.genericBuilder = builder + /** @suppress Internal function used by converter builders. **/ + public open fun withBuilder( + builder: ListConverterBuilder, + ): ListConverter { + this.builder = builder + this.genericBuilder = builder - return this - } + return this + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/OptionalCoalescingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/OptionalCoalescingConverter.kt index 9a09fcf114..5f86d40cfa 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/OptionalCoalescingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/OptionalCoalescingConverter.kt @@ -24,26 +24,26 @@ import com.kotlindiscord.kord.extensions.commands.converters.builders.OptionalCo * @property validator Validation lambda, which may throw a DiscordRelayedException if required. */ public abstract class OptionalCoalescingConverter( - public val outputError: Boolean = false, - override var validator: Validator = null + public val outputError: Boolean = false, + override var validator: Validator = null, ) : Converter, T?, List, Int>(false), SlashCommandConverter { - /** - * The parsed value. - * - * This should be set by the converter during the course of the [parse] function. - */ - public override var parsed: T? = null + /** + * The parsed value. + * + * This should be set by the converter during the course of the [parse] function. + */ + public override var parsed: T? = null - /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ - public open lateinit var builder: OptionalCoalescingConverterBuilder + /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ + public open lateinit var builder: OptionalCoalescingConverterBuilder - /** @suppress Internal function used by converter builders. **/ - public open fun withBuilder( - builder: OptionalCoalescingConverterBuilder - ): OptionalCoalescingConverter { - this.builder = builder - this.genericBuilder = builder + /** @suppress Internal function used by converter builders. **/ + public open fun withBuilder( + builder: OptionalCoalescingConverterBuilder, + ): OptionalCoalescingConverter { + this.builder = builder + this.genericBuilder = builder - return this - } + return this + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/OptionalConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/OptionalConverter.kt index c0edc853d3..38e2aa7d07 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/OptionalConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/OptionalConverter.kt @@ -17,26 +17,26 @@ import com.kotlindiscord.kord.extensions.commands.converters.builders.OptionalCo * @property validator Validation lambda, which may throw a DiscordRelayedException if required. */ public abstract class OptionalConverter( - public val outputError: Boolean = false, - override var validator: Validator = null + public val outputError: Boolean = false, + override var validator: Validator = null, ) : Converter(false), SlashCommandConverter { - /** - * The parsed value. - * - * This should be set by the converter during the course of the [parse] function. - */ - public override var parsed: T? = null + /** + * The parsed value. + * + * This should be set by the converter during the course of the [parse] function. + */ + public override var parsed: T? = null - /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ - public open lateinit var builder: OptionalConverterBuilder + /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ + public open lateinit var builder: OptionalConverterBuilder - /** @suppress Internal function used by converter builders. **/ - public open fun withBuilder( - builder: OptionalConverterBuilder - ): OptionalConverter { - this.builder = builder - this.genericBuilder = builder + /** @suppress Internal function used by converter builders. **/ + public open fun withBuilder( + builder: OptionalConverterBuilder, + ): OptionalConverter { + this.builder = builder + this.genericBuilder = builder - return this - } + return this + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleConverter.kt index 226a1f6282..99507371d2 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleConverter.kt @@ -22,136 +22,136 @@ import com.kotlindiscord.kord.extensions.commands.converters.builders.ConverterB * @property validator Validation lambda, which may throw a DiscordRelayedException if required. */ public abstract class SingleConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : Converter(true), SlashCommandConverter { - /** - * The parsed value. - * - * This should be set by the converter during the course of the [parse] function. - */ - public override lateinit var parsed: T + /** + * The parsed value. + * + * This should be set by the converter during the course of the [parse] function. + */ + public override lateinit var parsed: T - /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ - public open lateinit var builder: ConverterBuilder + /** Access to the converter builder, perhaps a bit more hacky than it should be but whatever. **/ + public open lateinit var builder: ConverterBuilder - /** @suppress Internal function used by converter builders. **/ - public open fun withBuilder( - builder: ConverterBuilder - ): SingleConverter { - this.builder = builder - this.genericBuilder = builder + /** @suppress Internal function used by converter builders. **/ + public open fun withBuilder( + builder: ConverterBuilder, + ): SingleConverter { + this.builder = builder + this.genericBuilder = builder - return this - } + return this + } - /** - * Wrap this single converter with a [SingleToListConverter], which is a special converter that will act like a - * [ListConverter] using the same logic of this converter. - * - * Your converter should be designed with this pattern in mind. If that's not possible, please override this - * function and throw an exception in the body. - * - * For more information on the parameters, see [Converter]. - * - * @param required Whether command parsing should fail if no arguments could be converted. - * - * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter - * provides. - * - * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a - * generated command signature. - * - * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter - * provides. - */ - @ConverterToMulti - public open fun toList( - required: Boolean = true, - signatureTypeString: String? = null, - showTypeInSignature: Boolean? = null, - errorTypeString: String? = null, - nestedValidator: Validator> = null - ): ListConverter = SingleToListConverter( - required, - this, - signatureTypeString, - showTypeInSignature, - errorTypeString, - nestedValidator - ) + /** + * Wrap this single converter with a [SingleToListConverter], which is a special converter that will act like a + * [ListConverter] using the same logic of this converter. + * + * Your converter should be designed with this pattern in mind. If that's not possible, please override this + * function and throw an exception in the body. + * + * For more information on the parameters, see [Converter]. + * + * @param required Whether command parsing should fail if no arguments could be converted. + * + * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter + * provides. + * + * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a + * generated command signature. + * + * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter + * provides. + */ + @ConverterToMulti + public open fun toList( + required: Boolean = true, + signatureTypeString: String? = null, + showTypeInSignature: Boolean? = null, + errorTypeString: String? = null, + nestedValidator: Validator> = null, + ): ListConverter = SingleToListConverter( + required, + this, + signatureTypeString, + showTypeInSignature, + errorTypeString, + nestedValidator + ) - /** - * Wrap this single converter with a [SingleToOptionalConverter], which is a special converter that will act like - * an [OptionalConverter] using the same logic of this converter. - * - * Your converter should be designed with this pattern in mind. If that's not possible, please override this - * function and throw an exception in the body. - * - * For more information on the parameters, see [Converter]. - * - * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter - * provides. - * - * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a - * generated command signature. - * - * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter - * provides. - * - * @param outputError Optionally, provide `true` to fail parsing and return errors if the converter throws a - * [DiscordRelayedException] (instead of continuing). - */ - @ConverterToOptional - public open fun toOptional( - signatureTypeString: String? = null, - showTypeInSignature: Boolean? = null, - errorTypeString: String? = null, - outputError: Boolean = false, - nestedValidator: Validator = null - ): OptionalConverter = SingleToOptionalConverter( - this, - signatureTypeString, - showTypeInSignature, - errorTypeString, - outputError, - nestedValidator - ) + /** + * Wrap this single converter with a [SingleToOptionalConverter], which is a special converter that will act like + * an [OptionalConverter] using the same logic of this converter. + * + * Your converter should be designed with this pattern in mind. If that's not possible, please override this + * function and throw an exception in the body. + * + * For more information on the parameters, see [Converter]. + * + * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter + * provides. + * + * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a + * generated command signature. + * + * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter + * provides. + * + * @param outputError Optionally, provide `true` to fail parsing and return errors if the converter throws a + * [DiscordRelayedException] (instead of continuing). + */ + @ConverterToOptional + public open fun toOptional( + signatureTypeString: String? = null, + showTypeInSignature: Boolean? = null, + errorTypeString: String? = null, + outputError: Boolean = false, + nestedValidator: Validator = null, + ): OptionalConverter = SingleToOptionalConverter( + this, + signatureTypeString, + showTypeInSignature, + errorTypeString, + outputError, + nestedValidator + ) - /** - * Wrap this single converter with a [SingleToDefaultingConverter], which is a special converter that will act like - * a [DefaultingConverter] using the same logic of this converter. - * - * Your converter should be designed with this pattern in mind. If that's not possible, please override this - * function and throw an exception in the body. - * - * For more information on the parameters, see [Converter]. - * - * @param defaultValue The default value to use when an argument can't be converted. - * @param outputError Whether the argument parser should output parsing errors on invalid arguments. - * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter - * provides. - * - * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a - * generated command signature. - * - * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter - * provides. - */ - @ConverterToDefaulting - public open fun toDefaulting( - defaultValue: T, - outputError: Boolean = false, - signatureTypeString: String? = null, - showTypeInSignature: Boolean? = null, - errorTypeString: String? = null, - nestedValidator: Validator = null - ): DefaultingConverter = SingleToDefaultingConverter( - this, - defaultValue = defaultValue, - outputError = outputError, - newSignatureTypeString = signatureTypeString, - newShowTypeInSignature = showTypeInSignature, - newErrorTypeString = errorTypeString, - validator = nestedValidator - ) + /** + * Wrap this single converter with a [SingleToDefaultingConverter], which is a special converter that will act like + * a [DefaultingConverter] using the same logic of this converter. + * + * Your converter should be designed with this pattern in mind. If that's not possible, please override this + * function and throw an exception in the body. + * + * For more information on the parameters, see [Converter]. + * + * @param defaultValue The default value to use when an argument can't be converted. + * @param outputError Whether the argument parser should output parsing errors on invalid arguments. + * @param signatureTypeString Optionally, a signature type string to use instead of the one this converter + * provides. + * + * @param showTypeInSignature Optionally, override this converter's setting for showing the type string in a + * generated command signature. + * + * @param errorTypeString Optionally, a longer type string to be shown in errors instead of the one this converter + * provides. + */ + @ConverterToDefaulting + public open fun toDefaulting( + defaultValue: T, + outputError: Boolean = false, + signatureTypeString: String? = null, + showTypeInSignature: Boolean? = null, + errorTypeString: String? = null, + nestedValidator: Validator = null, + ): DefaultingConverter = SingleToDefaultingConverter( + this, + defaultValue = defaultValue, + outputError = outputError, + newSignatureTypeString = signatureTypeString, + newShowTypeInSignature = showTypeInSignature, + newErrorTypeString = errorTypeString, + validator = nestedValidator + ) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToDefaultingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToDefaultingConverter.kt index 7439463723..b43dc01a15 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToDefaultingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToDefaultingConverter.kt @@ -25,58 +25,58 @@ import dev.kord.rest.builder.interaction.OptionsBuilder * @param newErrorTypeString An optional error type string to override the one set in [singleConverter]. */ public class SingleToDefaultingConverter( - public val singleConverter: SingleConverter, - defaultValue: T, - outputError: Boolean = false, + public val singleConverter: SingleConverter, + defaultValue: T, + outputError: Boolean = false, - newSignatureTypeString: String? = null, - newShowTypeInSignature: Boolean? = null, - newErrorTypeString: String? = null, + newSignatureTypeString: String? = null, + newShowTypeInSignature: Boolean? = null, + newErrorTypeString: String? = null, - override var validator: Validator = null + override var validator: Validator = null, ) : DefaultingConverter(defaultValue, outputError = outputError) { - override val signatureTypeString: String = newSignatureTypeString ?: singleConverter.signatureTypeString - override val showTypeInSignature: Boolean = newShowTypeInSignature ?: singleConverter.showTypeInSignature - override val errorTypeString: String? = newErrorTypeString ?: singleConverter.errorTypeString + override val signatureTypeString: String = newSignatureTypeString ?: singleConverter.signatureTypeString + override val showTypeInSignature: Boolean = newShowTypeInSignature ?: singleConverter.showTypeInSignature + override val errorTypeString: String? = newErrorTypeString ?: singleConverter.errorTypeString private val dummyArgs = Arguments() - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val token = parser?.peekNext() - val result = singleConverter.parse(parser, context, named ?: token?.data) + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val token = parser?.peekNext() + val result = singleConverter.parse(parser, context, named ?: token?.data) - if (result) { - this.parsed = singleConverter.getValue(dummyArgs, singleConverter::parsed) + if (result) { + this.parsed = singleConverter.getValue(dummyArgs, singleConverter::parsed) - if (named == null) { - parser?.parseNext() // Move the cursor ahead - } + if (named == null) { + parser?.parseNext() // Move the cursor ahead + } - return true - } + return true + } - return false - } + return false + } - override suspend fun handleError( - t: Throwable, - context: CommandContext - ): String = singleConverter.handleError(t, context) + override suspend fun handleError( + t: Throwable, + context: CommandContext, + ): String = singleConverter.handleError(t, context) - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder { - val option = singleConverter.toSlashOption(arg) - option.required = false + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder { + val option = singleConverter.toSlashOption(arg) + option.required = false - return option - } + return option + } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val result = singleConverter.parseOption(context, option) + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val result = singleConverter.parseOption(context, option) - if (result) { - this.parsed = singleConverter.getValue(dummyArgs, singleConverter::parsed) - } + if (result) { + this.parsed = singleConverter.getValue(dummyArgs, singleConverter::parsed) + } - return result - } + return result + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToListConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToListConverter.kt index 7bcd9d0e3d..287020544a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToListConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToListConverter.kt @@ -28,66 +28,66 @@ import dev.kord.rest.builder.interaction.OptionsBuilder * @param newErrorTypeString An optional error type string to override the one set in [singleConverter]. */ public class SingleToListConverter( - required: Boolean = true, - public val singleConverter: SingleConverter, + required: Boolean = true, + public val singleConverter: SingleConverter, - newSignatureTypeString: String? = null, - newShowTypeInSignature: Boolean? = null, - newErrorTypeString: String? = null, + newSignatureTypeString: String? = null, + newShowTypeInSignature: Boolean? = null, + newErrorTypeString: String? = null, - override var validator: Validator> = null + override var validator: Validator> = null, ) : ListConverter(required) { - override val signatureTypeString: String = newSignatureTypeString ?: singleConverter.signatureTypeString - override val showTypeInSignature: Boolean = newShowTypeInSignature ?: singleConverter.showTypeInSignature - override val errorTypeString: String? = newErrorTypeString ?: singleConverter.errorTypeString + override val signatureTypeString: String = newSignatureTypeString ?: singleConverter.signatureTypeString + override val showTypeInSignature: Boolean = newShowTypeInSignature ?: singleConverter.showTypeInSignature + override val errorTypeString: String? = newErrorTypeString ?: singleConverter.errorTypeString private val dummyArgs = Arguments() - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - val values = mutableListOf() + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + val values = mutableListOf() - if (named == null) { - while (true) { - val arg = parser?.peekNext()?.data + if (named == null) { + while (true) { + val arg = parser?.peekNext()?.data - try { - val result = singleConverter.parse(null, context, arg) + try { + val result = singleConverter.parse(null, context, arg) - if (!result) { - break - } + if (!result) { + break + } - val value = singleConverter.getValue(dummyArgs, singleConverter::parsed) + val value = singleConverter.getValue(dummyArgs, singleConverter::parsed) - values.add(value) + values.add(value) - parser?.parseNext() // Move the cursor ahead - } catch (e: DiscordRelayedException) { - break - } - } - } else { - for (arg in named) { - try { - val result = singleConverter.parse(null, context, arg) + parser?.parseNext() // Move the cursor ahead + } catch (e: DiscordRelayedException) { + break + } + } + } else { + for (arg in named) { + try { + val result = singleConverter.parse(null, context, arg) - if (!result) { - break - } + if (!result) { + break + } - val value = singleConverter.getValue(dummyArgs, singleConverter::parsed) + val value = singleConverter.getValue(dummyArgs, singleConverter::parsed) - values.add(value) - } catch (e: DiscordRelayedException) { - break - } - } - } + values.add(value) + } catch (e: DiscordRelayedException) { + break + } + } + } - parsed = values + parsed = values - return parsed.size - } + return parsed.size + } override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = singleConverter.toSlashOption(arg) @@ -103,7 +103,7 @@ public class SingleToListConverter( } override suspend fun handleError( - t: Throwable, - context: CommandContext - ): String = singleConverter.handleError(t, context) + t: Throwable, + context: CommandContext, + ): String = singleConverter.handleError(t, context) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToOptionalConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToOptionalConverter.kt index 0fc8e7fa72..f2787da42d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToOptionalConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SingleToOptionalConverter.kt @@ -60,7 +60,7 @@ public class SingleToOptionalConverter( override suspend fun handleError( t: Throwable, context: CommandContext, - ): String = singleConverter.handleError(t, context) + ): String = singleConverter.handleError(t, context) override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder { val option = singleConverter.toSlashOption(arg) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SlashCommandConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SlashCommandConverter.kt index a59443794f..e6bba4f713 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SlashCommandConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/SlashCommandConverter.kt @@ -15,13 +15,13 @@ import dev.kord.rest.builder.interaction.OptionsBuilder * Interface representing converters that can be made use of in slash commands. */ public interface SlashCommandConverter { - /** - * Return a slash command option that corresponds to this converter. - * - * Only applicable to converter types that make sense for slash commands. - */ - public suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder + /** + * Return a slash command option that corresponds to this converter. + * + * Only applicable to converter types that make sense for slash commands. + */ + public suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder - /** Use the given [option] taken straight from the slash command invocation to fill the converter. **/ - public suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean + /** Use the given [option] taken straight from the slash command invocation to fill the converter. **/ + public suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/_Types.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/_Types.kt index 879af294c2..0c1f96ba38 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/_Types.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/_Types.kt @@ -20,4 +20,4 @@ public typealias Mutator = ((value: T) -> T)? /** Type alias representing an autocomplete callable. **/ public typealias AutoCompleteCallback = - (suspend AutoCompleteInteraction.(event: AutoCompleteInteractionCreateEvent) -> Unit)? + (suspend AutoCompleteInteraction.(event: AutoCompleteInteractionCreateEvent) -> Unit)? diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ChoiceConverterBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ChoiceConverterBuilder.kt index f6bcebb7d3..501b1a574d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ChoiceConverterBuilder.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ChoiceConverterBuilder.kt @@ -10,16 +10,16 @@ import com.kotlindiscord.kord.extensions.utils.MutableStringKeyedMap /** Converter builder for choice converters. **/ public interface ChoiceConverterBuilder { - /** List of possible choices, if any. **/ - public var choices: MutableStringKeyedMap + /** List of possible choices, if any. **/ + public var choices: MutableStringKeyedMap - /** Add a choice to the list of possible choices. **/ - public fun choice(key: String, value: T) { - choices[key] = value - } + /** Add a choice to the list of possible choices. **/ + public fun choice(key: String, value: T) { + choices[key] = value + } - /** Add a choice to the list of possible choices. **/ - public fun choices(all: Map) { - choices = all.toMutableMap() - } + /** Add a choice to the list of possible choices. **/ + public fun choices(all: Map) { + choices = all.toMutableMap() + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ConverterBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ConverterBuilder.kt index 79e9acee8d..2e077ed3a7 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ConverterBuilder.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ConverterBuilder.kt @@ -16,64 +16,64 @@ import com.kotlindiscord.kord.extensions.commands.converters.Validator /** Base abstract class for all converter builders. **/ public abstract class ConverterBuilder { - /** Converter display name. Required. **/ - public open lateinit var name: String - - /** Converter description. Required. **/ - public open lateinit var description: String - - /** Mutator, used to mutate the parsed value before it's presented. **/ - public open var mutator: Mutator = null - - /** Validator, used for argument validation. **/ - protected open var validator: Validator = null - - /** Auto-complete callback. **/ - public open var autoCompleteCallback: AutoCompleteCallback = null - - /** Register the autocomplete callback for this converter. **/ - public open fun autoComplete(body: AutoCompleteCallback) { - autoCompleteCallback = body - } - - /** Register the mutator for this converter, allowing you to modify the final value. **/ - public open fun mutate(body: Mutator) { - mutator = body - } - - /** Register the validator for this converter, allowing you to validate the final value. **/ - public open fun validate(body: Validator) { - validator = body - } - - /** Using the data in this builder, create a converter and apply it to an [Arguments] object. **/ - public abstract fun build(arguments: Arguments): Converter<*, *, *, *> - - /** Validate that this builder is set up properly. **/ - public open fun validateArgument() { - if (!this::name.isInitialized) { - throw InvalidArgumentException(this, "Required field not provided: name") - } - - if (!this::description.isInitialized) { - throw InvalidArgumentException(this, "Required field not provided: description") - } - - if (this is ChoiceConverterBuilder<*> && this.choices.isNotEmpty() && this.autoCompleteCallback != null) { - throw InvalidArgumentException( - this, - "One of either a map of choices or an autocomplete callback may be provided, but both are present" - ) - } - } - - /** Validate that this builder's value is allowable. **/ - public open suspend fun validateValue(commandContext: CommandContext, value: T) { - if (validator != null) { - val context = ValidationContext(value, commandContext) - - validator?.invoke(context) - context.throwIfFailed() - } - } + /** Converter display name. Required. **/ + public open lateinit var name: String + + /** Converter description. Required. **/ + public open lateinit var description: String + + /** Mutator, used to mutate the parsed value before it's presented. **/ + public open var mutator: Mutator = null + + /** Validator, used for argument validation. **/ + protected open var validator: Validator = null + + /** Auto-complete callback. **/ + public open var autoCompleteCallback: AutoCompleteCallback = null + + /** Register the autocomplete callback for this converter. **/ + public open fun autoComplete(body: AutoCompleteCallback) { + autoCompleteCallback = body + } + + /** Register the mutator for this converter, allowing you to modify the final value. **/ + public open fun mutate(body: Mutator) { + mutator = body + } + + /** Register the validator for this converter, allowing you to validate the final value. **/ + public open fun validate(body: Validator) { + validator = body + } + + /** Using the data in this builder, create a converter and apply it to an [Arguments] object. **/ + public abstract fun build(arguments: Arguments): Converter<*, *, *, *> + + /** Validate that this builder is set up properly. **/ + public open fun validateArgument() { + if (!this::name.isInitialized) { + throw InvalidArgumentException(this, "Required field not provided: name") + } + + if (!this::description.isInitialized) { + throw InvalidArgumentException(this, "Required field not provided: description") + } + + if (this is ChoiceConverterBuilder<*> && this.choices.isNotEmpty() && this.autoCompleteCallback != null) { + throw InvalidArgumentException( + this, + "One of either a map of choices or an autocomplete callback may be provided, but both are present" + ) + } + } + + /** Validate that this builder's value is allowable. **/ + public open suspend fun validateValue(commandContext: CommandContext, value: T) { + if (validator != null) { + val context = ValidationContext(value, commandContext) + + validator?.invoke(context) + context.throwIfFailed() + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/DefaultingCoalescingConverterBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/DefaultingCoalescingConverterBuilder.kt index 89ac05a3bb..37a6563ed7 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/DefaultingCoalescingConverterBuilder.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/DefaultingCoalescingConverterBuilder.kt @@ -13,13 +13,13 @@ public abstract class DefaultingCoalescingConverterBuilder : Coalescing override var ignoreErrors: Boolean = false /** Value to use when none is provided, or when there's a parsing error and [ignoreErrors] is `true`. **/ - public open lateinit var defaultValue: T + public open lateinit var defaultValue: T - override fun validateArgument() { - super.validateArgument() + override fun validateArgument() { + super.validateArgument() - if (!this::defaultValue.isInitialized) { - throw InvalidArgumentException(this, "Required field not provided: defaultValue") - } - } + if (!this::defaultValue.isInitialized) { + throw InvalidArgumentException(this, "Required field not provided: defaultValue") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/DefaultingConverterBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/DefaultingConverterBuilder.kt index 4c14afb031..dcae78233b 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/DefaultingConverterBuilder.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/DefaultingConverterBuilder.kt @@ -10,17 +10,17 @@ import com.kotlindiscord.kord.extensions.InvalidArgumentException /** Converter builder for defaulting converters. **/ public abstract class DefaultingConverterBuilder : ConverterBuilder() { - /** Whether to ignore parsing errors when a value is provided. **/ - public var ignoreErrors: Boolean = false + /** Whether to ignore parsing errors when a value is provided. **/ + public var ignoreErrors: Boolean = false - /** Value to use when none is provided, or when there's a parsing error and [ignoreErrors] is `true`. **/ - public open lateinit var defaultValue: T + /** Value to use when none is provided, or when there's a parsing error and [ignoreErrors] is `true`. **/ + public open lateinit var defaultValue: T - override fun validateArgument() { - super.validateArgument() + override fun validateArgument() { + super.validateArgument() - if (!this::defaultValue.isInitialized) { - throw InvalidArgumentException(this, "Required field not provided: defaultValue") - } - } + if (!this::defaultValue.isInitialized) { + throw InvalidArgumentException(this, "Required field not provided: defaultValue") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ListConverterBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ListConverterBuilder.kt index f5a455d7ff..c129c3a6f7 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ListConverterBuilder.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ListConverterBuilder.kt @@ -8,6 +8,6 @@ package com.kotlindiscord.kord.extensions.commands.converters.builders /** Converter builder for list converters. **/ public abstract class ListConverterBuilder : ConverterBuilder>() { - /** Whether to ignore parsing errors when no values have been parsed out. **/ - public var ignoreErrors: Boolean = true + /** Whether to ignore parsing errors when no values have been parsed out. **/ + public var ignoreErrors: Boolean = true } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/OptionalConverterBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/OptionalConverterBuilder.kt index 56725983e8..1fb451a3bb 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/OptionalConverterBuilder.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/OptionalConverterBuilder.kt @@ -8,6 +8,6 @@ package com.kotlindiscord.kord.extensions.commands.converters.builders /** Converter builder for optional converters. **/ public abstract class OptionalConverterBuilder : ConverterBuilder() { - /** Whether to ignore parsing errors when a value is provided. **/ - public var ignoreErrors: Boolean = false + /** Whether to ignore parsing errors when a value is provided. **/ + public var ignoreErrors: Boolean = false } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ValidationContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ValidationContext.kt index 6f30bcad41..dc1339fa14 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ValidationContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/builders/ValidationContext.kt @@ -20,183 +20,183 @@ import java.util.* * @property context Command context that triggered this validation */ public class ValidationContext(public val value: T, public val context: CommandContext) : KordExKoinComponent { - /** - * Translation key to use for the error response message, if not the default. - * - * The string pointed to by this variable must accept one replacement value, which is the error message itself. - * - * **Note:** This *must* be a translation key. A bare string may not work, as the error response function uses - * the replacement functionality of the translations system. - */ - public var errorResponseKey: String = "checks.responseTemplate" - - /** Translation bundle used by [translate] by default and the error response translation, if not the default. **/ - public var defaultBundle: String? = context.command.resolvedBundle - - /** Human-readable message for the user, if any. **/ - public var message: String? = null - - /** Whether this validator has passed. **/ - public var passed: Boolean = true - - /** Mark this validator as having passed successfully. **/ - public fun pass() { - this.passed = true - } - - /** Mark this validator as having failed, optionally providing a message for the user. **/ - public fun fail(message: String? = null) { - this.message = message - this.passed = false - } - - /** - * If [value] is `true`, mark this validator as having failed, optionally providing a message for the user. - * - * Returns `true` if the validator was marked as having failed, `false` otherwise. - */ - public fun failIf(value: Boolean, message: String? = null): Boolean { - if (value) { - fail(message) - - return true - } - - return false - } - - /** - * If [callback] returns `true`, mark this validator as having failed, optionally providing a message for the user. - * - * Returns `true` if the validator was marked as having failed, `false` otherwise. - */ - public suspend fun failIf(message: String? = null, callback: suspend () -> Boolean): Boolean = - failIf(callback(), message) - - /** - * If [value] is `false`, mark this validator as having failed, optionally providing a message for the user. - * - * Returns `true` if the validator was marked as having failed, `false` otherwise. - */ - public fun failIfNot(value: Boolean, message: String? = null): Boolean = - failIf(!value, message) - - /** - * If [callback] returns `false`, mark this validator as having failed, optionally providing a message for the user. - * - * Returns `true` if the validator was marked as having failed, `false` otherwise. - */ - public suspend fun failIfNot(message: String? = null, callback: suspend () -> Boolean): Boolean = - failIfNot(callback(), message) - - /** - * If [value] is `true`, mark this validator as having passed. - * - * Returns `true` if the validator was marked as having passed, `false` otherwise. - */ - public fun passIf(value: Boolean): Boolean { - if (value) { - pass() - - return true - } - - return false - } - - /** - * If [callback] returns `true`, mark this validator as having passed. - * - * Returns `true` if the validator was marked as having passed, `false` otherwise. - */ - public suspend fun passIf(callback: suspend () -> Boolean): Boolean = - passIf(callback()) - - /** - * If [value] is `true`, mark this validator as having passed. - * - * Returns `true` if the validator was marked as having passed, `false` otherwise. - */ - public fun passIfNot(value: Boolean): Boolean = - passIf(!value) - - /** - * If [callback] returns `true`, mark this validator as having passed. - * - * Returns `true` if the validator was marked as having passed, `false` otherwise. - */ - public suspend fun passIfNot(callback: suspend () -> Boolean): Boolean = - passIfNot(callback()) - - /** Call the given block if the Boolean receiver is `true`. **/ - public inline fun Boolean.whenTrue(body: () -> T?): T? { - if (this) { - return body() - } - - return null - } - - /** Call the given block if the Boolean receiver is `false`. **/ - public inline fun Boolean.whenFalse(body: () -> T?): T? { - if (!this) { - return body() - } - - return null - } - - /** Quick access to translate strings using this validator context's locale. **/ - public suspend fun translate( - key: String, - bundle: String? = defaultBundle, - replacements: Array = arrayOf(), - ): String = - context.translate(key, bundleName = bundle, replacements = replacements) - - /** Quick access to translate strings using this validator context's locale. **/ - public suspend fun translate( - key: String, - replacements: Array = arrayOf(), - ): String = - context.translate(key, bundleName = defaultBundle, replacements = replacements) - - /** Quick access to translate strings using this validator context's locale. **/ - public suspend fun translate( - key: String, - replacements: Map, - ): String = - context.translate(key, bundleName = defaultBundle, replacements = replacements) - - /** Quick access to translate strings using this validator context's locale. **/ - public suspend fun translate( - key: String, - bundle: String?, - replacements: Map, - ): String = - context.translate(key, bundleName = bundle, replacements = replacements) - - /** - * If this validator has failed, throw a [DiscordRelayedException] with the translated message, if any. - */ - @Throws(DiscordRelayedException::class) - public suspend fun throwIfFailed() { - if (passed.not()) { - if (message != null) { - throw DiscordRelayedException( - getTranslatedMessage()!! - ) - } else { - error("Validation failed.") - } - } - } - - /** Get the translated validator failure message, if the validator has failed and a message was set. **/ - public suspend fun getTranslatedMessage(): String? = - if (passed.not() && message != null) { - translate(errorResponseKey, defaultBundle, replacements = arrayOf(message)) - } else { - null - } + /** + * Translation key to use for the error response message, if not the default. + * + * The string pointed to by this variable must accept one replacement value, which is the error message itself. + * + * **Note:** This *must* be a translation key. A bare string may not work, as the error response function uses + * the replacement functionality of the translations system. + */ + public var errorResponseKey: String = "checks.responseTemplate" + + /** Translation bundle used by [translate] by default and the error response translation, if not the default. **/ + public var defaultBundle: String? = context.command.resolvedBundle + + /** Human-readable message for the user, if any. **/ + public var message: String? = null + + /** Whether this validator has passed. **/ + public var passed: Boolean = true + + /** Mark this validator as having passed successfully. **/ + public fun pass() { + this.passed = true + } + + /** Mark this validator as having failed, optionally providing a message for the user. **/ + public fun fail(message: String? = null) { + this.message = message + this.passed = false + } + + /** + * If [value] is `true`, mark this validator as having failed, optionally providing a message for the user. + * + * Returns `true` if the validator was marked as having failed, `false` otherwise. + */ + public fun failIf(value: Boolean, message: String? = null): Boolean { + if (value) { + fail(message) + + return true + } + + return false + } + + /** + * If [callback] returns `true`, mark this validator as having failed, optionally providing a message for the user. + * + * Returns `true` if the validator was marked as having failed, `false` otherwise. + */ + public suspend fun failIf(message: String? = null, callback: suspend () -> Boolean): Boolean = + failIf(callback(), message) + + /** + * If [value] is `false`, mark this validator as having failed, optionally providing a message for the user. + * + * Returns `true` if the validator was marked as having failed, `false` otherwise. + */ + public fun failIfNot(value: Boolean, message: String? = null): Boolean = + failIf(!value, message) + + /** + * If [callback] returns `false`, mark this validator as having failed, optionally providing a message for the user. + * + * Returns `true` if the validator was marked as having failed, `false` otherwise. + */ + public suspend fun failIfNot(message: String? = null, callback: suspend () -> Boolean): Boolean = + failIfNot(callback(), message) + + /** + * If [value] is `true`, mark this validator as having passed. + * + * Returns `true` if the validator was marked as having passed, `false` otherwise. + */ + public fun passIf(value: Boolean): Boolean { + if (value) { + pass() + + return true + } + + return false + } + + /** + * If [callback] returns `true`, mark this validator as having passed. + * + * Returns `true` if the validator was marked as having passed, `false` otherwise. + */ + public suspend fun passIf(callback: suspend () -> Boolean): Boolean = + passIf(callback()) + + /** + * If [value] is `true`, mark this validator as having passed. + * + * Returns `true` if the validator was marked as having passed, `false` otherwise. + */ + public fun passIfNot(value: Boolean): Boolean = + passIf(!value) + + /** + * If [callback] returns `true`, mark this validator as having passed. + * + * Returns `true` if the validator was marked as having passed, `false` otherwise. + */ + public suspend fun passIfNot(callback: suspend () -> Boolean): Boolean = + passIfNot(callback()) + + /** Call the given block if the Boolean receiver is `true`. **/ + public inline fun Boolean.whenTrue(body: () -> T?): T? { + if (this) { + return body() + } + + return null + } + + /** Call the given block if the Boolean receiver is `false`. **/ + public inline fun Boolean.whenFalse(body: () -> T?): T? { + if (!this) { + return body() + } + + return null + } + + /** Quick access to translate strings using this validator context's locale. **/ + public suspend fun translate( + key: String, + bundle: String? = defaultBundle, + replacements: Array = arrayOf(), + ): String = + context.translate(key, bundleName = bundle, replacements = replacements) + + /** Quick access to translate strings using this validator context's locale. **/ + public suspend fun translate( + key: String, + replacements: Array = arrayOf(), + ): String = + context.translate(key, bundleName = defaultBundle, replacements = replacements) + + /** Quick access to translate strings using this validator context's locale. **/ + public suspend fun translate( + key: String, + replacements: Map, + ): String = + context.translate(key, bundleName = defaultBundle, replacements = replacements) + + /** Quick access to translate strings using this validator context's locale. **/ + public suspend fun translate( + key: String, + bundle: String?, + replacements: Map, + ): String = + context.translate(key, bundleName = bundle, replacements = replacements) + + /** + * If this validator has failed, throw a [DiscordRelayedException] with the translated message, if any. + */ + @Throws(DiscordRelayedException::class) + public suspend fun throwIfFailed() { + if (passed.not()) { + if (message != null) { + throw DiscordRelayedException( + getTranslatedMessage()!! + ) + } else { + error("Validation failed.") + } + } + } + + /** Get the translated validator failure message, if the validator has failed and a message was set. **/ + public suspend fun getTranslatedMessage(): String? = + if (passed.not() && message != null) { + translate(errorResponseKey, defaultBundle, replacements = arrayOf(message)) + } else { + null + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/AttachmentConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/AttachmentConverter.kt index 604ec07e1f..2fb7031f7e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/AttachmentConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/AttachmentConverter.kt @@ -27,26 +27,26 @@ import dev.kord.rest.builder.interaction.OptionsBuilder * This converter can only be used in slash commands. */ @Converter( - "attachment", + "attachment", - types = [ConverterType.OPTIONAL, ConverterType.SINGLE], + types = [ConverterType.OPTIONAL, ConverterType.SINGLE], ) public class AttachmentConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.attachment.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.attachment.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean = - throw DiscordRelayedException(context.translate("converters.attachment.error.slashCommandsOnly")) + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean = + throw DiscordRelayedException(context.translate("converters.attachment.error.slashCommandsOnly")) - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - AttachmentBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + AttachmentBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? AttachmentOptionValue)?.resolvedObject ?: return false - this.parsed = optionValue + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? AttachmentOptionValue)?.resolvedObject ?: return false + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/BooleanConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/BooleanConverter.kt index e2c5d86b0a..2f709a0b2e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/BooleanConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/BooleanConverter.kt @@ -26,33 +26,33 @@ import dev.kord.rest.builder.interaction.OptionsBuilder * Truthiness is determined by the [parseBoolean] function. */ @Converter( - "boolean", + "boolean", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] ) public class BooleanConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - public override val signatureTypeString: String = "converters.boolean.signatureType" - public override val errorTypeString: String = "converters.boolean.errorType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + public override val signatureTypeString: String = "converters.boolean.signatureType" + public override val errorTypeString: String = "converters.boolean.errorType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false - val bool: Boolean = arg.parseBoolean(context) ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false + val bool: Boolean = arg.parseBoolean(context) ?: return false - this.parsed = bool + this.parsed = bool - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - BooleanBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + BooleanBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? BooleanOptionValue)?.value ?: return false - this.parsed = optionValue + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? BooleanOptionValue)?.value ?: return false + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DecimalConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DecimalConverter.kt index 7798e69f46..2a4ea95b55 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DecimalConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DecimalConverter.kt @@ -30,68 +30,68 @@ import dev.kord.rest.builder.interaction.OptionsBuilder * @see decimalList */ @Converter( - "decimal", + "decimal", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - builderFields = [ - "public var maxValue: Double? = null", - "public var minValue: Double? = null", - ], + builderFields = [ + "public var maxValue: Double? = null", + "public var minValue: Double? = null", + ], ) public class DecimalConverter( - public val maxValue: Double? = null, - public val minValue: Double? = null, + public val maxValue: Double? = null, + public val minValue: Double? = null, - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.decimal.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.decimal.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - try { - this.parsed = arg.toDouble() - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate("converters.decimal.error.invalid", replacements = arrayOf(arg)) - ) - } + try { + this.parsed = arg.toDouble() + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate("converters.decimal.error.invalid", replacements = arrayOf(arg)) + ) + } - if (minValue != null && this.parsed < minValue) { - throw DiscordRelayedException( - context.translate( - "converters.number.error.invalid.tooSmall", - replacements = arrayOf(arg, minValue) - ) - ) - } + if (minValue != null && this.parsed < minValue) { + throw DiscordRelayedException( + context.translate( + "converters.number.error.invalid.tooSmall", + replacements = arrayOf(arg, minValue) + ) + ) + } - if (maxValue != null && this.parsed > maxValue) { - throw DiscordRelayedException( - context.translate( - "converters.number.error.invalid.tooLarge", - replacements = arrayOf(arg, maxValue) - ) - ) - } + if (maxValue != null && this.parsed > maxValue) { + throw DiscordRelayedException( + context.translate( + "converters.number.error.invalid.tooLarge", + replacements = arrayOf(arg, maxValue) + ) + ) + } - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - NumberOptionBuilder(arg.displayName, arg.description).apply { - this@apply.maxValue = this@DecimalConverter.maxValue - this@apply.minValue = this@DecimalConverter.minValue + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + NumberOptionBuilder(arg.displayName, arg.description).apply { + this@apply.maxValue = this@DecimalConverter.maxValue + this@apply.minValue = this@DecimalConverter.minValue - required = true - } + required = true + } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? NumberOptionValue)?.value ?: return false - this.parsed = optionValue + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? NumberOptionValue)?.value ?: return false + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DurationCoalescingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DurationCoalescingConverter.kt index 74dfa95b30..21f7f046d4 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DurationCoalescingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DurationCoalescingConverter.kt @@ -35,193 +35,193 @@ import kotlinx.datetime.* * @param positiveOnly Whether a positive duration is required - `true` by default. */ @Converter( - names = ["duration"], - types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL], - imports = ["kotlinx.datetime.*"], - - builderFields = [ - "public var longHelp: Boolean = true", - "public var positiveOnly: Boolean = true", - ], + names = ["duration"], + types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL], + imports = ["kotlinx.datetime.*"], + + builderFields = [ + "public var longHelp: Boolean = true", + "public var positiveOnly: Boolean = true", + ], ) public class DurationCoalescingConverter( - public val longHelp: Boolean = true, - public val positiveOnly: Boolean = true, - shouldThrow: Boolean = false, - override var validator: Validator = null + public val longHelp: Boolean = true, + public val positiveOnly: Boolean = true, + shouldThrow: Boolean = false, + override var validator: Validator = null, ) : CoalescingConverter(shouldThrow) { - override val signatureTypeString: String = "converters.duration.error.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.duration.error.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - private val logger = KotlinLogging.logger {} + private val logger = KotlinLogging.logger {} - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - // Check if it's a discord-formatted timestamp first - val timestamp = - (named?.getOrNull(0) ?: parser?.peekNext()?.data)?.let { TimestampConverter.parseFromString(it) } - if (timestamp != null) { - val result = (timestamp.instant - Clock.System.now()).toDateTimePeriod() + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + // Check if it's a discord-formatted timestamp first + val timestamp = + (named?.getOrNull(0) ?: parser?.peekNext()?.data)?.let { TimestampConverter.parseFromString(it) } + if (timestamp != null) { + val result = (timestamp.instant - Clock.System.now()).toDateTimePeriod() - checkPositive(context, result, positiveOnly) + checkPositive(context, result, positiveOnly) - this.parsed = result + this.parsed = result - return 1 - } + return 1 + } - val durations = mutableListOf() + val durations = mutableListOf() - val ignoredWords: List = context.translate("utils.durations.ignoredWords") - .split(",") - .toMutableList() - .apply { remove(EMPTY_VALUE_STRING) } + val ignoredWords: List = context.translate("utils.durations.ignoredWords") + .split(",") + .toMutableList() + .apply { remove(EMPTY_VALUE_STRING) } - var skipNext = false + var skipNext = false - val args: List = named ?: parser?.run { - val tokens: MutableList = mutableListOf() + val args: List = named ?: parser?.run { + val tokens: MutableList = mutableListOf() - while (hasNext) { - val nextToken: PositionalArgumentToken? = peekNext() + while (hasNext) { + val nextToken: PositionalArgumentToken? = peekNext() - if (nextToken!!.data.all { DurationParser.charValid(it, context.getLocale()) }) { - tokens.add(parseNext()!!.data) - } else { - break - } - } - - tokens - } ?: return 0 - - @Suppress("LoopWithTooManyJumpStatements") // Well you rewrite it then, detekt - for (index in args.indices) { - if (skipNext) { - skipNext = false + if (nextToken!!.data.all { DurationParser.charValid(it, context.getLocale()) }) { + tokens.add(parseNext()!!.data) + } else { + break + } + } + + tokens + } ?: return 0 + + @Suppress("LoopWithTooManyJumpStatements") // Well you rewrite it then, detekt + for (index in args.indices) { + if (skipNext) { + skipNext = false - continue - } + continue + } - val arg: String = args[index] + val arg: String = args[index] - if (arg in ignoredWords) continue + if (arg in ignoredWords) continue - try { - // We do it this way so that we stop parsing as soon as an invalid string is found - DurationParser.parse(arg, context.getLocale()) - DurationParser.parse(durations.joinToString("") + arg, context.getLocale()) + try { + // We do it this way so that we stop parsing as soon as an invalid string is found + DurationParser.parse(arg, context.getLocale()) + DurationParser.parse(durations.joinToString("") + arg, context.getLocale()) - durations.add(arg) - } catch (e: DurationParserException) { - try { - val nextIndex: Int = index + 1 - - if (nextIndex >= args.size) { - throw e - } - - val nextArg: String = args[nextIndex] - val combined: String = arg + nextArg - - DurationParser.parse(combined, context.getLocale()) - DurationParser.parse(durations.joinToString("") + combined, context.getLocale()) - - durations.add(combined) - skipNext = true - } catch (t: InvalidTimeUnitException) { - throwIfNecessary(t, context) - - break - } catch (t: DurationParserException) { - throwIfNecessary(t, context) - - break - } - } - } - - try { - val result: DateTimePeriod = DurationParser.parse( - durations.joinToString(""), - context.getLocale() - ) - - checkPositive(context, result, positiveOnly) - - parsed = result - } catch (e: InvalidTimeUnitException) { - throwIfNecessary(e, context, true) - } catch (e: DurationParserException) { - throwIfNecessary(e, context, true) - } - - return durations.size - } - - private suspend fun throwIfNecessary( - e: Exception, - context: CommandContext, - override: Boolean = false - ): Unit = if (shouldThrow || override) { - when (e) { - is InvalidTimeUnitException -> { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - - throw DiscordRelayedException(message) - } - - is DurationParserException -> throw DiscordRelayedException(e.error) - - else -> throw e - } - } else { - logger.debug(e) { "Error thrown during duration parsing" } - } - - private suspend inline fun checkPositive(context: CommandContext, result: DateTimePeriod, positiveOnly: Boolean) { - if (positiveOnly) { - val now: Instant = Clock.System.now() - val applied: Instant = now.plus(result, TimeZone.UTC) - - if (now > applied) { - throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) - } - } - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - - try { - val result: DateTimePeriod = DurationParser.parse(optionValue, context.getLocale()) - - if (positiveOnly) { - val now: Instant = Clock.System.now() - val applied: Instant = now.plus(result, TimeZone.UTC) - - if (now > applied) { - throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) - } - } - - parsed = result - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } - - return true - } + durations.add(arg) + } catch (e: DurationParserException) { + try { + val nextIndex: Int = index + 1 + + if (nextIndex >= args.size) { + throw e + } + + val nextArg: String = args[nextIndex] + val combined: String = arg + nextArg + + DurationParser.parse(combined, context.getLocale()) + DurationParser.parse(durations.joinToString("") + combined, context.getLocale()) + + durations.add(combined) + skipNext = true + } catch (t: InvalidTimeUnitException) { + throwIfNecessary(t, context) + + break + } catch (t: DurationParserException) { + throwIfNecessary(t, context) + + break + } + } + } + + try { + val result: DateTimePeriod = DurationParser.parse( + durations.joinToString(""), + context.getLocale() + ) + + checkPositive(context, result, positiveOnly) + + parsed = result + } catch (e: InvalidTimeUnitException) { + throwIfNecessary(e, context, true) + } catch (e: DurationParserException) { + throwIfNecessary(e, context, true) + } + + return durations.size + } + + private suspend fun throwIfNecessary( + e: Exception, + context: CommandContext, + override: Boolean = false, + ): Unit = if (shouldThrow || override) { + when (e) { + is InvalidTimeUnitException -> { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + + throw DiscordRelayedException(message) + } + + is DurationParserException -> throw DiscordRelayedException(e.error) + + else -> throw e + } + } else { + logger.debug(e) { "Error thrown during duration parsing" } + } + + private suspend inline fun checkPositive(context: CommandContext, result: DateTimePeriod, positiveOnly: Boolean) { + if (positiveOnly) { + val now: Instant = Clock.System.now() + val applied: Instant = now.plus(result, TimeZone.UTC) + + if (now > applied) { + throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) + } + } + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + + try { + val result: DateTimePeriod = DurationParser.parse(optionValue, context.getLocale()) + + if (positiveOnly) { + val now: Instant = Clock.System.now() + val applied: Instant = now.plus(result, TimeZone.UTC) + + if (now > applied) { + throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) + } + } + + parsed = result + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } + + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DurationConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DurationConverter.kt index c742d39522..4999955a5c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DurationConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/DurationConverter.kt @@ -33,89 +33,89 @@ import kotlinx.datetime.* * @param positiveOnly Whether a positive duration is required - `true` by default. */ @Converter( - names = ["duration"], - types = [ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE], - imports = ["kotlinx.datetime.*"], - - builderFields = [ - "public var longHelp: Boolean = true", - "public var positiveOnly: Boolean = true" - ], + names = ["duration"], + types = [ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE], + imports = ["kotlinx.datetime.*"], + + builderFields = [ + "public var longHelp: Boolean = true", + "public var positiveOnly: Boolean = true" + ], ) public class DurationConverter( - public val longHelp: Boolean = true, - public val positiveOnly: Boolean = true, - override var validator: Validator = null + public val longHelp: Boolean = true, + public val positiveOnly: Boolean = true, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.duration.error.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE - - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false - - try { - // Check if it's a discord-formatted timestamp first - val timestamp = TimestampConverter.parseFromString(arg) - val result: DateTimePeriod = if (timestamp == null) { - DurationParser.parse(arg, context.getLocale()) - } else { - (timestamp.instant - Clock.System.now()).toDateTimePeriod() - } - - if (positiveOnly) { - val now: Instant = Clock.System.now() - val applied: Instant = now.plus(result, TimeZone.UTC) - - if (now > applied) { - throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) - } - } - - parsed = result - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } - - return true - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - - try { - val result: DateTimePeriod = DurationParser.parse(optionValue, context.getLocale()) - - if (positiveOnly) { - val now: Instant = Clock.System.now() - val applied: Instant = now.plus(result, TimeZone.UTC) - - if (now > applied) { - throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) - } - } - - parsed = result - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } - - return true - } + override val signatureTypeString: String = "converters.duration.error.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE + + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false + + try { + // Check if it's a discord-formatted timestamp first + val timestamp = TimestampConverter.parseFromString(arg) + val result: DateTimePeriod = if (timestamp == null) { + DurationParser.parse(arg, context.getLocale()) + } else { + (timestamp.instant - Clock.System.now()).toDateTimePeriod() + } + + if (positiveOnly) { + val now: Instant = Clock.System.now() + val applied: Instant = now.plus(result, TimeZone.UTC) + + if (now > applied) { + throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) + } + } + + parsed = result + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } + + return true + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + + try { + val result: DateTimePeriod = DurationParser.parse(optionValue, context.getLocale()) + + if (positiveOnly) { + val now: Instant = Clock.System.now() + val applied: Instant = now.plus(result, TimeZone.UTC) + + if (now > applied) { + throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) + } + } + + parsed = result + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } + + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/EmailConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/EmailConverter.kt index b80397a04d..9e6697a2b9 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/EmailConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/EmailConverter.kt @@ -25,44 +25,44 @@ import org.apache.commons.validator.routines.EmailValidator * Argument converter for email address arguments. */ @Converter( - "email", + "email", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] ) public class EmailConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.email.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.email.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - if (!EmailValidator.getInstance().isValid(arg)) { - throw DiscordRelayedException( - context.translate("converters.email.error.invalid", replacements = arrayOf(arg)) - ) - } + if (!EmailValidator.getInstance().isValid(arg)) { + throw DiscordRelayedException( + context.translate("converters.email.error.invalid", replacements = arrayOf(arg)) + ) + } - this.parsed = arg + this.parsed = arg - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false - if (!EmailValidator.getInstance().isValid(optionValue)) { - throw DiscordRelayedException( - context.translate("converters.email.error.invalid", replacements = arrayOf(optionValue)) - ) - } + if (!EmailValidator.getInstance().isValid(optionValue)) { + throw DiscordRelayedException( + context.translate("converters.email.error.invalid", replacements = arrayOf(optionValue)) + ) + } - this.parsed = optionValue + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/EnumConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/EnumConverter.kt index 9c7765782e..72c6067e7d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/EnumConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/EnumConverter.kt @@ -30,66 +30,66 @@ import dev.kord.rest.builder.interaction.StringChoiceBuilder * @see enumList */ @Converter( - "enum", + "enum", - types = [ConverterType.SINGLE, ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.LIST], - imports = ["com.kotlindiscord.kord.extensions.commands.converters.impl.getEnum"], + types = [ConverterType.SINGLE, ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.LIST], + imports = ["com.kotlindiscord.kord.extensions.commands.converters.impl.getEnum"], - builderGeneric = "E: Enum", - builderConstructorArguments = [ - "public var getter: suspend (String) -> E?" - ], + builderGeneric = "E: Enum", + builderConstructorArguments = [ + "public var getter: suspend (String) -> E?" + ], - builderFields = [ - "public lateinit var typeName: String", - "public var bundle: String? = null" - ], + builderFields = [ + "public lateinit var typeName: String", + "public var bundle: String? = null" + ], - functionGeneric = "E: Enum", - functionBuilderArguments = [ - "getter = { getEnum(it) }", - ] + functionGeneric = "E: Enum", + functionBuilderArguments = [ + "getter = { getEnum(it) }", + ] ) public class EnumConverter>( - typeName: String, - private val getter: suspend (String) -> E?, - override val bundle: String? = DEFAULT_KORDEX_BUNDLE, - override var validator: Validator = null + typeName: String, + private val getter: suspend (String) -> E?, + override val bundle: String? = DEFAULT_KORDEX_BUNDLE, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = typeName + override val signatureTypeString: String = typeName - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - try { - parsed = getter.invoke(arg) ?: return false - } catch (e: IllegalArgumentException) { - return false - } + try { + parsed = getter.invoke(arg) ?: return false + } catch (e: IllegalArgumentException) { + return false + } - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false - try { - parsed = getter.invoke(optionValue) ?: return false - } catch (e: IllegalArgumentException) { - return false - } + try { + parsed = getter.invoke(optionValue) ?: return false + } catch (e: IllegalArgumentException) { + return false + } - return true - } + return true + } } /** * The default enum value getter - matches enums based on a case-insensitive string comparison with the name. */ public inline fun > getEnum(arg: String): E? = - enumValues().firstOrNull { - it.name.equals(arg, true) - } + enumValues().firstOrNull { + it.name.equals(arg, true) + } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/GuildConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/GuildConverter.kt index dd6d0c686e..3f9809721d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/GuildConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/GuildConverter.kt @@ -35,57 +35,57 @@ import kotlinx.coroutines.flow.firstOrNull * @see guildList */ @Converter( - "guild", + "guild", - types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] + types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] ) public class GuildConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.guild.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.guild.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - if (arg.equals("this", true)) { - val guild = context.getGuild()?.asGuildOrNull() + if (arg.equals("this", true)) { + val guild = context.getGuild()?.asGuildOrNull() - if (guild != null) { - this.parsed = guild + if (guild != null) { + this.parsed = guild - return true - } - } + return true + } + } - this.parsed = findGuild(arg) - ?: throw DiscordRelayedException( - context.translate("converters.guild.error.missing", replacements = arrayOf(arg)) - ) + this.parsed = findGuild(arg) + ?: throw DiscordRelayedException( + context.translate("converters.guild.error.missing", replacements = arrayOf(arg)) + ) - return true - } + return true + } - private suspend fun findGuild(arg: String): Guild? = - try { // Try for a guild ID first - val id = Snowflake(arg) + private suspend fun findGuild(arg: String): Guild? = + try { // Try for a guild ID first + val id = Snowflake(arg) - kord.getGuildOrNull(id) - } catch (e: NumberFormatException) { // It's not an ID, let's try the name - kord.guilds.firstOrNull { it.name.equals(arg, true) } - } + kord.getGuildOrNull(id) + } catch (e: NumberFormatException) { // It's not an ID, let's try the name + kord.guilds.firstOrNull { it.name.equals(arg, true) } + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false - this.parsed = findGuild(optionValue) - ?: throw DiscordRelayedException( - context.translate("converters.guild.error.missing", replacements = arrayOf(optionValue)) - ) + this.parsed = findGuild(optionValue) + ?: throw DiscordRelayedException( + context.translate("converters.guild.error.missing", replacements = arrayOf(optionValue)) + ) - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/IntConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/IntConverter.kt index 7793f4ca1a..ff4a14ccc8 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/IntConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/IntConverter.kt @@ -29,75 +29,75 @@ private const val DEFAULT_RADIX = 10 * @property minValue The minimum value allowed for this argument. */ @Converter( - "int", + "int", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - builderFields = [ - "public var radix: Int = $DEFAULT_RADIX", + builderFields = [ + "public var radix: Int = $DEFAULT_RADIX", - "public var maxValue: Int? = null", - "public var minValue: Int? = null", - ] + "public var maxValue: Int? = null", + "public var minValue: Int? = null", + ] ) public class IntConverter( - private val radix: Int = DEFAULT_RADIX, - public val maxValue: Int? = null, - public val minValue: Int? = null, + private val radix: Int = DEFAULT_RADIX, + public val maxValue: Int? = null, + public val minValue: Int? = null, - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.number.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE - - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false - - try { - this.parsed = arg.toInt(radix) - } catch (e: NumberFormatException) { - val errorString = if (radix == DEFAULT_RADIX) { - context.translate("converters.number.error.invalid.defaultBase", replacements = arrayOf(arg)) - } else { - context.translate("converters.number.error.invalid.otherBase", replacements = arrayOf(arg, radix)) - } - - throw DiscordRelayedException(errorString) - } - - if (minValue != null && this.parsed < minValue) { - throw DiscordRelayedException( - context.translate( - "converters.number.error.invalid.tooSmall", - replacements = arrayOf(arg, minValue) - ) - ) - } - - if (maxValue != null && this.parsed > maxValue) { - throw DiscordRelayedException( - context.translate( - "converters.number.error.invalid.tooLarge", - replacements = arrayOf(arg, maxValue) - ) - ) - } - - return true - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - IntegerOptionBuilder(arg.displayName, arg.description).apply { - this@apply.maxValue = this@IntConverter.maxValue?.toLong() - this@apply.minValue = this@IntConverter.minValue?.toLong() - - required = true - } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? IntegerOptionValue)?.value ?: return false - this.parsed = optionValue.toInt() - - return true - } + override val signatureTypeString: String = "converters.number.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE + + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false + + try { + this.parsed = arg.toInt(radix) + } catch (e: NumberFormatException) { + val errorString = if (radix == DEFAULT_RADIX) { + context.translate("converters.number.error.invalid.defaultBase", replacements = arrayOf(arg)) + } else { + context.translate("converters.number.error.invalid.otherBase", replacements = arrayOf(arg, radix)) + } + + throw DiscordRelayedException(errorString) + } + + if (minValue != null && this.parsed < minValue) { + throw DiscordRelayedException( + context.translate( + "converters.number.error.invalid.tooSmall", + replacements = arrayOf(arg, minValue) + ) + ) + } + + if (maxValue != null && this.parsed > maxValue) { + throw DiscordRelayedException( + context.translate( + "converters.number.error.invalid.tooLarge", + replacements = arrayOf(arg, maxValue) + ) + ) + } + + return true + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + IntegerOptionBuilder(arg.displayName, arg.description).apply { + this@apply.maxValue = this@IntConverter.maxValue?.toLong() + this@apply.minValue = this@IntConverter.minValue?.toLong() + + required = true + } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? IntegerOptionValue)?.value ?: return false + this.parsed = optionValue.toInt() + + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/LongConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/LongConverter.kt index e108cc2605..aaa8999ec3 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/LongConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/LongConverter.kt @@ -29,75 +29,75 @@ private const val DEFAULT_RADIX = 10 * @property minValue The minimum value allowed for this argument. */ @Converter( - "long", + "long", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - builderFields = [ - "public var radix: Int = $DEFAULT_RADIX", + builderFields = [ + "public var radix: Int = $DEFAULT_RADIX", - "public var maxValue: Long? = null", - "public var minValue: Long? = null", - ] + "public var maxValue: Long? = null", + "public var minValue: Long? = null", + ] ) public class LongConverter( - private val radix: Int = DEFAULT_RADIX, - public val maxValue: Long? = null, - public val minValue: Long? = null, + private val radix: Int = DEFAULT_RADIX, + public val maxValue: Long? = null, + public val minValue: Long? = null, - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.number.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE - - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false - - try { - this.parsed = arg.toLong(radix) - } catch (e: NumberFormatException) { - val errorString = if (radix == DEFAULT_RADIX) { - context.translate("converters.number.error.invalid.defaultBase", replacements = arrayOf(arg)) - } else { - context.translate("converters.number.error.invalid.otherBase", replacements = arrayOf(arg, radix)) - } - - throw DiscordRelayedException(errorString) - } - - if (minValue != null && this.parsed < minValue) { - throw DiscordRelayedException( - context.translate( - "converters.number.error.invalid.tooSmall", - replacements = arrayOf(arg, minValue) - ) - ) - } - - if (maxValue != null && this.parsed > maxValue) { - throw DiscordRelayedException( - context.translate( - "converters.number.error.invalid.tooLarge", - replacements = arrayOf(arg, maxValue) - ) - ) - } - - return true - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - IntegerOptionBuilder(arg.displayName, arg.description).apply { - this@apply.maxValue = this@LongConverter.maxValue - this@apply.minValue = this@LongConverter.minValue - - required = true - } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? IntegerOptionValue)?.value ?: return false - this.parsed = optionValue - - return true - } + override val signatureTypeString: String = "converters.number.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE + + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false + + try { + this.parsed = arg.toLong(radix) + } catch (e: NumberFormatException) { + val errorString = if (radix == DEFAULT_RADIX) { + context.translate("converters.number.error.invalid.defaultBase", replacements = arrayOf(arg)) + } else { + context.translate("converters.number.error.invalid.otherBase", replacements = arrayOf(arg, radix)) + } + + throw DiscordRelayedException(errorString) + } + + if (minValue != null && this.parsed < minValue) { + throw DiscordRelayedException( + context.translate( + "converters.number.error.invalid.tooSmall", + replacements = arrayOf(arg, minValue) + ) + ) + } + + if (maxValue != null && this.parsed > maxValue) { + throw DiscordRelayedException( + context.translate( + "converters.number.error.invalid.tooLarge", + replacements = arrayOf(arg, maxValue) + ) + ) + } + + return true + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + IntegerOptionBuilder(arg.displayName, arg.description).apply { + this@apply.maxValue = this@LongConverter.maxValue + this@apply.minValue = this@LongConverter.minValue + + required = true + } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? IntegerOptionValue)?.value ?: return false + this.parsed = optionValue + + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/MemberConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/MemberConverter.kt index f382a3beb0..6eab880a0a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/MemberConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/MemberConverter.kt @@ -43,59 +43,59 @@ import kotlinx.coroutines.flow.firstOrNull * argument. */ @Converter( - "member", + "member", - types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - imports = ["dev.kord.common.entity.Snowflake"], + types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + imports = ["dev.kord.common.entity.Snowflake"], - builderFields = [ - "public var requiredGuild: (suspend () -> Snowflake)? = null", - "public var useReply: Boolean = true", - "public var requireSameGuild: Boolean = true", - ] + builderFields = [ + "public var requiredGuild: (suspend () -> Snowflake)? = null", + "public var useReply: Boolean = true", + "public var requireSameGuild: Boolean = true", + ] ) public class MemberConverter( - private var requiredGuild: (suspend () -> Snowflake)? = null, - private var useReply: Boolean = true, - private var requireSameGuild: Boolean = true, - override var validator: Validator = null, + private var requiredGuild: (suspend () -> Snowflake)? = null, + private var useReply: Boolean = true, + private var requireSameGuild: Boolean = true, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.member.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.member.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val guild = context.getGuild() + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val guild = context.getGuild() - if (requireSameGuild && requiredGuild == null && guild != null) { - requiredGuild = { guild.id } - } + if (requireSameGuild && requiredGuild == null && guild != null) { + requiredGuild = { guild.id } + } - if (useReply && context is ChatCommandContext<*>) { - val messageReference = context.message.asMessage().messageReference + if (useReply && context is ChatCommandContext<*>) { + val messageReference = context.message.asMessage().messageReference - if (messageReference != null) { - val member = messageReference.message?.asMessage()?.getAuthorAsMemberOrNull() + if (messageReference != null) { + val member = messageReference.message?.asMessage()?.getAuthorAsMemberOrNull() - if (member != null) { - parsed = member - return true - } - } - } + if (member != null) { + parsed = member + return true + } + } + } - val arg: String = named ?: parser?.parseNext()?.data ?: return false + val arg: String = named ?: parser?.parseNext()?.data ?: return false - if (arg.equals("me", true)) { - val member = context.getMember()?.asMemberOrNull() + if (arg.equals("me", true)) { + val member = context.getMember()?.asMemberOrNull() - if (member != null) { - this.parsed = member + if (member != null) { + this.parsed = member - return true - } - } + return true + } + } - if (arg.equals("you", true) && guild != null) { + if (arg.equals("you", true) && guild != null) { val member = bot.kordRef.getSelf().asMemberOrNull(guild.id) if (member != null) { @@ -103,42 +103,42 @@ public class MemberConverter( return true } - } - - parsed = findMember(arg, context) - ?: throw DiscordRelayedException( - context.translate("converters.member.error.missing", replacements = arrayOf(arg)) - ) - - return true - } - - private suspend fun findMember(arg: String, context: CommandContext): Member? { - val user: User? = if (arg.startsWith("<@") && arg.endsWith(">")) { // It's a mention - val id: String = arg.substring(2, arg.length - 1).replace("!", "") - - try { - kord.getUser(Snowflake(id)) - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate("converters.member.error.invalid", replacements = arrayOf(id)) - ) - } - } else { - try { // Try for a user ID first - kord.getUser(Snowflake(arg)) - } catch (e: NumberFormatException) { // It's not an ID, let's try the tag - if (!arg.contains("#")) { - null - } else { - kord.users.firstOrNull { user -> - user.tag.equals(arg, true) - } - } - } - } - - val currentGuild = context.getGuild() + } + + parsed = findMember(arg, context) + ?: throw DiscordRelayedException( + context.translate("converters.member.error.missing", replacements = arrayOf(arg)) + ) + + return true + } + + private suspend fun findMember(arg: String, context: CommandContext): Member? { + val user: User? = if (arg.startsWith("<@") && arg.endsWith(">")) { // It's a mention + val id: String = arg.substring(2, arg.length - 1).replace("!", "") + + try { + kord.getUser(Snowflake(id)) + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate("converters.member.error.invalid", replacements = arrayOf(id)) + ) + } + } else { + try { // Try for a user ID first + kord.getUser(Snowflake(arg)) + } catch (e: NumberFormatException) { // It's not an ID, let's try the tag + if (!arg.contains("#")) { + null + } else { + kord.users.firstOrNull { user -> + user.tag.equals(arg, true) + } + } + } + } + + val currentGuild = context.getGuild() if (requiredGuild != null || requireSameGuild) { val requiredGuildId: Snowflake = requiredGuild?.invoke() @@ -156,37 +156,37 @@ public class MemberConverter( currentGuild?.id ?: return null ) - } + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - UserBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + UserBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = if (context.eventObj is AutoCompleteInteractionCreateEvent) { - val id = (option as? MemberOptionValue)?.value ?: return false - val guild = context.getGuild() ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = if (context.eventObj is AutoCompleteInteractionCreateEvent) { + val id = (option as? MemberOptionValue)?.value ?: return false + val guild = context.getGuild() ?: return false - kord.getUser(id)?.asMemberOrNull(guild.id) ?: return false - } else { - (option as? MemberOptionValue)?.resolvedObject ?: return false - } + kord.getUser(id)?.asMemberOrNull(guild.id) ?: return false + } else { + (option as? MemberOptionValue)?.resolvedObject ?: return false + } - val guild = context.getGuild() + val guild = context.getGuild() - if (requireSameGuild && requiredGuild == null && guild != null) { - requiredGuild = { guild.id } - } + if (requireSameGuild && requiredGuild == null && guild != null) { + requiredGuild = { guild.id } + } - val requiredGuildId = requiredGuild?.invoke() + val requiredGuildId = requiredGuild?.invoke() - if (requiredGuildId != null && optionValue.guildId != requiredGuildId) { - throw DiscordRelayedException( - context.translate("converters.member.error.invalid", replacements = arrayOf(optionValue.username)) - ) - } + if (requiredGuildId != null && optionValue.guildId != requiredGuildId) { + throw DiscordRelayedException( + context.translate("converters.member.error.invalid", replacements = arrayOf(optionValue.username)) + ) + } - this.parsed = optionValue + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/MessageConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/MessageConverter.kt index e8f0c3d307..a3c8bdbbae 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/MessageConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/MessageConverter.kt @@ -45,167 +45,167 @@ private val logger = KotlinLogging.logger {} * @param useReply Whether to use the replied-to message (if there is one) instead of trying to parse an argument. */ @Converter( - "message", + "message", - types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - imports = ["dev.kord.common.entity.Snowflake"], + types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + imports = ["dev.kord.common.entity.Snowflake"], - builderFields = [ - "public var requireGuild: Boolean = false", - "public var requiredGuild: (suspend () -> Snowflake)? = null", - "public var useReply: Boolean = true", - ] + builderFields = [ + "public var requireGuild: Boolean = false", + "public var requiredGuild: (suspend () -> Snowflake)? = null", + "public var useReply: Boolean = true", + ] ) public class MessageConverter( - private var requireGuild: Boolean = false, - private var requiredGuild: (suspend () -> Snowflake)? = null, - private var useReply: Boolean = true, - override var validator: Validator = null + private var requireGuild: Boolean = false, + private var requiredGuild: (suspend () -> Snowflake)? = null, + private var useReply: Boolean = true, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.message.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE - - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - if (useReply && context is ChatCommandContext<*>) { - val messageReference = context.message.asMessage().messageReference - - if (messageReference != null) { - val message = messageReference.message?.asMessageOrNull() - - if (message != null) { - parsed = message - return true - } - } - } - - val arg: String = named ?: parser?.parseNext()?.data ?: return false - - parsed = findMessage(arg, context) - - return true - } - - private suspend fun findMessage(arg: String, context: CommandContext): Message { - val requiredGid: Snowflake? = if (requiredGuild != null) { - requiredGuild!!.invoke() - } else { - context.getGuild()?.id - } - - return if (arg.startsWith("https://")) { // It's a message URL - @Suppress("MagicNumber") - val split: List = arg.substring(8).split("/").takeLast(3) - - @Suppress("MagicNumber") - if (split.size < 3) { - throw DiscordRelayedException( - context.translate("converters.message.error.invalidUrl", replacements = arrayOf(arg)) - ) - } - - @Suppress("MagicNumber") - val gid: Snowflake = try { - Snowflake(split[0]) - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate("converters.message.error.invalidGuildId", replacements = arrayOf(split[0])) - ) - } - - if (requireGuild && requiredGid != gid) { - logger.trace { "Matching guild ($requiredGid) required, but guild ($gid) doesn't match." } - - errorNoMessage(arg, context) - } - - @Suppress("MagicNumber") - val cid: Snowflake = try { - Snowflake(split[1]) - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate( - "converters.message.error.invalidChannelId", - replacements = arrayOf(split[1]) - ) - ) - } - - val channel: GuildChannel? = kord.getGuildOrNull(gid)?.getChannel(cid) - - if (channel == null) { - logger.trace { "Unable to find channel ($cid) for guild ($gid)." } - - errorNoMessage(arg, context) - } - - if (channel !is GuildMessageChannel) { - logger.trace { "Specified channel ($cid) is not a guild message channel." } - - errorNoMessage(arg, context) - } - - @Suppress("MagicNumber") - val mid: Snowflake = try { - Snowflake(split[2]) - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate( - "converters.message.error.invalidMessageId", - replacements = arrayOf(split[2]) - ) - ) - } - - try { - channel.getMessage(mid) - } catch (e: EntityNotFoundException) { - errorNoMessage(mid.toString(), context) - } - } else { // Try a message ID - val channel: ChannelBehavior = context.getChannel() - - if (channel !is GuildMessageChannel && channel !is DmChannel) { - logger.trace { "Current channel is not a guild message channel or DM channel." } - - errorNoMessage(arg, context) - } - - if (channel !is MessageChannel) { - logger.trace { "Current channel is not a message channel, so it can't contain messages." } - - errorNoMessage(arg, context) - } - - try { - channel.getMessage(Snowflake(arg)) - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate( - "converters.message.error.invalidMessageId", - replacements = arrayOf(arg) - ) - ) - } catch (e: EntityNotFoundException) { - errorNoMessage(arg, context) - } - } - } - - private suspend fun errorNoMessage(arg: String, context: CommandContext): Nothing { - throw DiscordRelayedException( - context.translate("converters.message.error.missing", replacements = arrayOf(arg)) - ) - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - - parsed = findMessage(optionValue, context) - - return true - } + override val signatureTypeString: String = "converters.message.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE + + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + if (useReply && context is ChatCommandContext<*>) { + val messageReference = context.message.asMessage().messageReference + + if (messageReference != null) { + val message = messageReference.message?.asMessageOrNull() + + if (message != null) { + parsed = message + return true + } + } + } + + val arg: String = named ?: parser?.parseNext()?.data ?: return false + + parsed = findMessage(arg, context) + + return true + } + + private suspend fun findMessage(arg: String, context: CommandContext): Message { + val requiredGid: Snowflake? = if (requiredGuild != null) { + requiredGuild!!.invoke() + } else { + context.getGuild()?.id + } + + return if (arg.startsWith("https://")) { // It's a message URL + @Suppress("MagicNumber") + val split: List = arg.substring(8).split("/").takeLast(3) + + @Suppress("MagicNumber") + if (split.size < 3) { + throw DiscordRelayedException( + context.translate("converters.message.error.invalidUrl", replacements = arrayOf(arg)) + ) + } + + @Suppress("MagicNumber") + val gid: Snowflake = try { + Snowflake(split[0]) + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate("converters.message.error.invalidGuildId", replacements = arrayOf(split[0])) + ) + } + + if (requireGuild && requiredGid != gid) { + logger.trace { "Matching guild ($requiredGid) required, but guild ($gid) doesn't match." } + + errorNoMessage(arg, context) + } + + @Suppress("MagicNumber") + val cid: Snowflake = try { + Snowflake(split[1]) + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate( + "converters.message.error.invalidChannelId", + replacements = arrayOf(split[1]) + ) + ) + } + + val channel: GuildChannel? = kord.getGuildOrNull(gid)?.getChannel(cid) + + if (channel == null) { + logger.trace { "Unable to find channel ($cid) for guild ($gid)." } + + errorNoMessage(arg, context) + } + + if (channel !is GuildMessageChannel) { + logger.trace { "Specified channel ($cid) is not a guild message channel." } + + errorNoMessage(arg, context) + } + + @Suppress("MagicNumber") + val mid: Snowflake = try { + Snowflake(split[2]) + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate( + "converters.message.error.invalidMessageId", + replacements = arrayOf(split[2]) + ) + ) + } + + try { + channel.getMessage(mid) + } catch (e: EntityNotFoundException) { + errorNoMessage(mid.toString(), context) + } + } else { // Try a message ID + val channel: ChannelBehavior = context.getChannel() + + if (channel !is GuildMessageChannel && channel !is DmChannel) { + logger.trace { "Current channel is not a guild message channel or DM channel." } + + errorNoMessage(arg, context) + } + + if (channel !is MessageChannel) { + logger.trace { "Current channel is not a message channel, so it can't contain messages." } + + errorNoMessage(arg, context) + } + + try { + channel.getMessage(Snowflake(arg)) + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate( + "converters.message.error.invalidMessageId", + replacements = arrayOf(arg) + ) + ) + } catch (e: EntityNotFoundException) { + errorNoMessage(arg, context) + } + } + } + + private suspend fun errorNoMessage(arg: String, context: CommandContext): Nothing { + throw DiscordRelayedException( + context.translate("converters.message.error.missing", replacements = arrayOf(arg)) + ) + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + + parsed = findMessage(optionValue, context) + + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RegexCoalescingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RegexCoalescingConverter.kt index 6a06ccce51..65b80e464d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RegexCoalescingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RegexCoalescingConverter.kt @@ -32,36 +32,36 @@ import dev.kord.rest.builder.interaction.StringChoiceBuilder * @see coalescedRegex */ @Converter( - "regex", + "regex", - types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL], - imports = ["kotlin.text.RegexOption"], - builderFields = ["public var options: Set = setOf()"] + types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL], + imports = ["kotlin.text.RegexOption"], + builderFields = ["public var options: Set = setOf()"] ) public class RegexCoalescingConverter( - private val options: Set = setOf(), - shouldThrow: Boolean = false, - override var validator: Validator = null + private val options: Set = setOf(), + shouldThrow: Boolean = false, + override var validator: Validator = null, ) : CoalescingConverter(shouldThrow) { - override val signatureTypeString: String = "converters.regex.signatureType.plural" - override val showTypeInSignature: Boolean = false - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.regex.signatureType.plural" + override val showTypeInSignature: Boolean = false + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - val args: String = named?.joinToString(" ") ?: parser?.consumeRemaining() ?: return 0 + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + val args: String = named?.joinToString(" ") ?: parser?.consumeRemaining() ?: return 0 - this.parsed = args.toRegex(options) + this.parsed = args.toRegex(options) - return args.length - } + return args.length + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - this.parsed = optionValue.toRegex(options) + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + this.parsed = optionValue.toRegex(options) - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RegexConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RegexConverter.kt index 62c7c03d00..23453ca55f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RegexConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RegexConverter.kt @@ -30,34 +30,34 @@ import dev.kord.rest.builder.interaction.StringChoiceBuilder * @param options Optional set of [RegexOption]s to pass to the regex parser. */ @Converter( - "regex", + "regex", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - imports = ["kotlin.text.RegexOption"], - builderFields = ["public var options: MutableSet = mutableSetOf()"] + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + imports = ["kotlin.text.RegexOption"], + builderFields = ["public var options: MutableSet = mutableSetOf()"] ) public class RegexConverter( - private val options: Set = setOf(), - override var validator: Validator = null + private val options: Set = setOf(), + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.regex.signatureType.singular" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.regex.signatureType.singular" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - this.parsed = arg.toRegex(options) + this.parsed = arg.toRegex(options) - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - this.parsed = optionValue.toRegex(options) + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + this.parsed = optionValue.toRegex(options) - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RoleConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RoleConverter.kt index 1aed126d44..13485e4727 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RoleConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/RoleConverter.kt @@ -37,76 +37,76 @@ import kotlinx.coroutines.flow.firstOrNull * guild the command was invoked in. */ @Converter( - "role", + "role", - types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - imports = ["dev.kord.common.entity.Snowflake"], - builderFields = ["public var requiredGuild: (suspend () -> Snowflake)? = null"] + types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + imports = ["dev.kord.common.entity.Snowflake"], + builderFields = ["public var requiredGuild: (suspend () -> Snowflake)? = null"] ) public class RoleConverter( - private var requiredGuild: (suspend () -> Snowflake)? = null, - override var validator: Validator = null + private var requiredGuild: (suspend () -> Snowflake)? = null, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.role.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE - - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false - - parsed = findRole(arg, context) - ?: throw DiscordRelayedException( - context.translate("converters.role.error.missing", replacements = arrayOf(arg)) - ) - - return true - } - - private suspend fun findRole(arg: String, context: CommandContext): Role? { - val guildId: Snowflake = if (requiredGuild != null) { - requiredGuild!!.invoke() - } else { - context.getGuild()?.id - } ?: return null - - val guild: Guild = kord.getGuildOrNull(guildId) ?: return null - - @Suppress("MagicNumber") - return if (arg.startsWith("<@&") && arg.endsWith(">")) { // It's a mention - val id: String = arg.substring(3, arg.length - 1) - - try { - guild.getRole(Snowflake(id)) - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate("converters.role.error.invalid", replacements = arrayOf(id)) - ) - } - } else { - try { // Try for a role ID first - guild.getRole(Snowflake(arg)) - } catch (e: NumberFormatException) { // It's not an ID, let's try the name - guild.roles.firstOrNull { role -> - role.name.equals(arg, true) - } - } - } - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - RoleBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = if (context.eventObj is AutoCompleteInteractionCreateEvent) { - val id = (option as? RoleOptionValue)?.value ?: return false - val guild = context.getGuild() ?: return false - - guild.getRoleOrNull(id) ?: return false - } else { - (option as? RoleOptionValue)?.resolvedObject ?: return false - } - - this.parsed = optionValue - - return true - } + override val signatureTypeString: String = "converters.role.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE + + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false + + parsed = findRole(arg, context) + ?: throw DiscordRelayedException( + context.translate("converters.role.error.missing", replacements = arrayOf(arg)) + ) + + return true + } + + private suspend fun findRole(arg: String, context: CommandContext): Role? { + val guildId: Snowflake = if (requiredGuild != null) { + requiredGuild!!.invoke() + } else { + context.getGuild()?.id + } ?: return null + + val guild: Guild = kord.getGuildOrNull(guildId) ?: return null + + @Suppress("MagicNumber") + return if (arg.startsWith("<@&") && arg.endsWith(">")) { // It's a mention + val id: String = arg.substring(3, arg.length - 1) + + try { + guild.getRole(Snowflake(id)) + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate("converters.role.error.invalid", replacements = arrayOf(id)) + ) + } + } else { + try { // Try for a role ID first + guild.getRole(Snowflake(arg)) + } catch (e: NumberFormatException) { // It's not an ID, let's try the name + guild.roles.firstOrNull { role -> + role.name.equals(arg, true) + } + } + } + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + RoleBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = if (context.eventObj is AutoCompleteInteractionCreateEvent) { + val id = (option as? RoleOptionValue)?.value ?: return false + val guild = context.getGuild() ?: return false + + guild.getRoleOrNull(id) ?: return false + } else { + (option as? RoleOptionValue)?.resolvedObject ?: return false + } + + this.parsed = optionValue + + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/SnowflakeConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/SnowflakeConverter.kt index af36084c82..6f32107fcb 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/SnowflakeConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/SnowflakeConverter.kt @@ -30,29 +30,29 @@ import dev.kord.rest.builder.interaction.StringChoiceBuilder * @see snowflakeList */ @Converter( - "snowflake", + "snowflake", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] ) public class SnowflakeConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.snowflake.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.snowflake.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - try { - this.parsed = Snowflake(arg) - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate("converters.snowflake.error.invalid", replacements = arrayOf(arg)) - ) - } + try { + this.parsed = Snowflake(arg) + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate("converters.snowflake.error.invalid", replacements = arrayOf(arg)) + ) + } - return true - } + return true + } override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/StringCoalescingConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/StringCoalescingConverter.kt index ae0e116da2..3e46897c21 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/StringCoalescingConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/StringCoalescingConverter.kt @@ -27,31 +27,31 @@ import dev.kord.rest.builder.interaction.StringChoiceBuilder * @see coalescedString */ @Converter( - "string", + "string", - types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL] + types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL] ) public class StringCoalescingConverter( - shouldThrow: Boolean = false, - override var validator: Validator = null + shouldThrow: Boolean = false, + override var validator: Validator = null, ) : CoalescingConverter(shouldThrow) { - override val signatureTypeString: String = "converters.string.signatureType" - override val showTypeInSignature: Boolean = false - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.string.signatureType" + override val showTypeInSignature: Boolean = false + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - this.parsed = named?.joinToString(" ") ?: parser?.consumeRemaining() ?: return 0 + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + this.parsed = named?.joinToString(" ") ?: parser?.consumeRemaining() ?: return 0 - return parsed.length - } + return parsed.length + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - this.parsed = optionValue + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/StringConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/StringConverter.kt index 85ceb2f163..4c46a8d79c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/StringConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/StringConverter.kt @@ -29,62 +29,62 @@ import dev.kord.rest.builder.interaction.StringChoiceBuilder * @property minLength The minimum length allowed for this argument. */ @Converter( - "string", + "string", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - builderFields = [ - "public var maxLength: Int? = null", - "public var minLength: Int? = null", - ] + builderFields = [ + "public var maxLength: Int? = null", + "public var minLength: Int? = null", + ] ) public class StringConverter( - public val maxLength: Int? = null, - public val minLength: Int? = null, - override var validator: Validator = null + public val maxLength: Int? = null, + public val minLength: Int? = null, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.string.signatureType" - override val showTypeInSignature: Boolean = false - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.string.signatureType" + override val showTypeInSignature: Boolean = false + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - this.parsed = arg + this.parsed = arg - if (minLength != null && this.parsed.length < minLength) { - throw DiscordRelayedException( - context.translate( - "converters.string.error.invalid.tooLong", - replacements = arrayOf(arg, minLength) - ) - ) - } + if (minLength != null && this.parsed.length < minLength) { + throw DiscordRelayedException( + context.translate( + "converters.string.error.invalid.tooLong", + replacements = arrayOf(arg, minLength) + ) + ) + } - if (maxLength != null && this.parsed.length > maxLength) { - throw DiscordRelayedException( - context.translate( - "converters.string.error.invalid.tooShort", - replacements = arrayOf(arg, maxLength) - ) - ) - } + if (maxLength != null && this.parsed.length > maxLength) { + throw DiscordRelayedException( + context.translate( + "converters.string.error.invalid.tooShort", + replacements = arrayOf(arg, maxLength) + ) + ) + } - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { - this@apply.maxLength = this@StringConverter.maxLength - this@apply.minLength = this@StringConverter.minLength + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { + this@apply.maxLength = this@StringConverter.maxLength + this@apply.minLength = this@StringConverter.minLength - required = true - } + required = true + } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - this.parsed = optionValue + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + this.parsed = optionValue - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/SupportedLocaleConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/SupportedLocaleConverter.kt index efe689a6da..5b526dca20 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/SupportedLocaleConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/SupportedLocaleConverter.kt @@ -35,35 +35,35 @@ import java.util.* * [our Weblate project](https://hosted.weblate.org/projects/kord-extensions/main/). */ @Converter( - "supportedLocale", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + "supportedLocale", + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], ) public class SupportedLocaleConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.supportedLocale.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.supportedLocale.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - this.parsed = SupportedLocales.ALL_LOCALES[arg.lowercase().trim()] ?: throw DiscordRelayedException( - context.translate("converters.supportedLocale.error.unknown", replacements = arrayOf(arg)) - ) + this.parsed = SupportedLocales.ALL_LOCALES[arg.lowercase().trim()] ?: throw DiscordRelayedException( + context.translate("converters.supportedLocale.error.unknown", replacements = arrayOf(arg)) + ) - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false - this.parsed = SupportedLocales.ALL_LOCALES[optionValue.lowercase().trim()] ?: throw DiscordRelayedException( - context.translate("converters.supportedLocale.error.unknown", replacements = arrayOf(optionValue)) - ) + this.parsed = SupportedLocales.ALL_LOCALES[optionValue.lowercase().trim()] ?: throw DiscordRelayedException( + context.translate("converters.supportedLocale.error.unknown", replacements = arrayOf(optionValue)) + ) - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TagConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TagConverter.kt index c6430578bd..34119bf206 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TagConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TagConverter.kt @@ -36,140 +36,140 @@ import org.koin.core.component.inject * Accepts a callable [channelGetter] property which may be used to extract a forum channel from another argument. */ @Converter( - "tag", - types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - imports = [ - "com.kotlindiscord.kord.extensions.utils.suggestStringCollection", - "dev.kord.core.entity.channel.ForumChannel", - ], - builderFields = [ - "public var channelGetter: (suspend () -> ForumChannel?)? = null" - ], - builderInitStatements = [ - "" + - " autoComplete { event ->\n" + - " try {\n" + - " val tags = TagConverter.getTags(event, channelGetter)\n" + - " \n" + - " suggestStringCollection(tags.map { it.name })\n" + - " } catch (e: Exception) {\n" + - " // kordLogger.warn{ \"Failed to process autocomplete event for tag converter: " + - "\${e.reason}\" }\n" + - " }\n" + - " }" - ] + "tag", + types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + imports = [ + "com.kotlindiscord.kord.extensions.utils.suggestStringCollection", + "dev.kord.core.entity.channel.ForumChannel", + ], + builderFields = [ + "public var channelGetter: (suspend () -> ForumChannel?)? = null" + ], + builderInitStatements = [ + "" + + " autoComplete { event ->\n" + + " try {\n" + + " val tags = TagConverter.getTags(event, channelGetter)\n" + + " \n" + + " suggestStringCollection(tags.map { it.name })\n" + + " } catch (e: Exception) {\n" + + " // kordLogger.warn{ \"Failed to process autocomplete event for tag converter: " + + "\${e.reason}\" }\n" + + " }\n" + + " }" + ] ) @Suppress("UnusedPrivateMember") public class TagConverter( - private val channelGetter: (suspend () -> ForumChannel?)? = null, + private val channelGetter: (suspend () -> ForumChannel?)? = null, - override var validator: Validator = null, + override var validator: Validator = null, ) : SingleConverter() { - public override val signatureTypeString: String = "converters.tag.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE - - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val input: String = named ?: parser?.parseNext()?.data ?: return false - - this.parsed = getTag(input, context) - - return true - } - - private suspend fun getTag(input: String, context: CommandContext): ForumTag { - val tags: List = getTags(context, channelGetter) - val locale: ULocale = ULocale(context.getLocale().toString()) - - val tag: ForumTag = tags.firstOrNull { - it.name.equals(input, true) - } ?: tags.firstOrNull { - if (locale.isRightToLeft) { - it.name.endsWith(input, true) - } else { - it.name.startsWith(input, true) - } - } ?: tags.firstOrNull { - it.name.contains(input, true) - } ?: throw DiscordRelayedException( - context.translate( - "converters.tag.error.unknownTag", - replacements = arrayOf(input) - ) - ) - - return tag - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue: String = (option as? StringOptionValue)?.value ?: return false - - this.parsed = getTag(optionValue, context) - - return true - } - - public companion object : KordExKoinComponent { - /** Translations provider, for retrieving translations. **/ - private val translationsProvider: TranslationsProvider by inject() - - public suspend fun getTags( - context: CommandContext, - getter: (suspend () -> ForumChannel?)? = null, - ): List { - val channel: ForumChannel? = if (getter != null) { - getter() - } else { - val thread = context.getChannel().asChannelOfOrNull() - - thread?.parent?.asChannelOfOrNull() - } - - if (channel == null) { - throw DiscordRelayedException( - context.translate( - if (getter == null) { - "converters.tag.error.wrongChannelType" - } else { - "converters.tag.error.wrongChannelTypeWithGetter" - } - ) - ) - } - - return channel.availableTags - } - - public suspend fun getTags( - event: AutoCompleteInteractionCreateEvent, - getter: (suspend () -> ForumChannel?)? = null, - ): List { - val channel: ForumChannel? = if (getter != null) { - getter() - } else { - val thread = event.interaction.getChannel().asChannelOfOrNull() - - thread?.parent?.asChannelOfOrNull() - } - - if (channel == null) { - throw DiscordRelayedException( - translationsProvider.translate( - if (getter == null) { - "converters.tag.error.wrongChannelType" - } else { - "converters.tag.error.wrongChannelTypeWithGetter" - }, - - event.getLocale(), - DEFAULT_KORDEX_BUNDLE - ) - ) - } - - return channel.availableTags - } - } + public override val signatureTypeString: String = "converters.tag.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE + + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val input: String = named ?: parser?.parseNext()?.data ?: return false + + this.parsed = getTag(input, context) + + return true + } + + private suspend fun getTag(input: String, context: CommandContext): ForumTag { + val tags: List = getTags(context, channelGetter) + val locale: ULocale = ULocale(context.getLocale().toString()) + + val tag: ForumTag = tags.firstOrNull { + it.name.equals(input, true) + } ?: tags.firstOrNull { + if (locale.isRightToLeft) { + it.name.endsWith(input, true) + } else { + it.name.startsWith(input, true) + } + } ?: tags.firstOrNull { + it.name.contains(input, true) + } ?: throw DiscordRelayedException( + context.translate( + "converters.tag.error.unknownTag", + replacements = arrayOf(input) + ) + ) + + return tag + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue: String = (option as? StringOptionValue)?.value ?: return false + + this.parsed = getTag(optionValue, context) + + return true + } + + public companion object : KordExKoinComponent { + /** Translations provider, for retrieving translations. **/ + private val translationsProvider: TranslationsProvider by inject() + + public suspend fun getTags( + context: CommandContext, + getter: (suspend () -> ForumChannel?)? = null, + ): List { + val channel: ForumChannel? = if (getter != null) { + getter() + } else { + val thread = context.getChannel().asChannelOfOrNull() + + thread?.parent?.asChannelOfOrNull() + } + + if (channel == null) { + throw DiscordRelayedException( + context.translate( + if (getter == null) { + "converters.tag.error.wrongChannelType" + } else { + "converters.tag.error.wrongChannelTypeWithGetter" + } + ) + ) + } + + return channel.availableTags + } + + public suspend fun getTags( + event: AutoCompleteInteractionCreateEvent, + getter: (suspend () -> ForumChannel?)? = null, + ): List { + val channel: ForumChannel? = if (getter != null) { + getter() + } else { + val thread = event.interaction.getChannel().asChannelOfOrNull() + + thread?.parent?.asChannelOfOrNull() + } + + if (channel == null) { + throw DiscordRelayedException( + translationsProvider.translate( + if (getter == null) { + "converters.tag.error.wrongChannelType" + } else { + "converters.tag.error.wrongChannelTypeWithGetter" + }, + + event.getLocale(), + DEFAULT_KORDEX_BUNDLE + ) + ) + } + + return channel.availableTags + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TimestampConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TimestampConverter.kt index 36982296d3..6e982f64cd 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TimestampConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TimestampConverter.kt @@ -30,59 +30,59 @@ private const val TIMESTAMP_SUFFIX = ">" * Argument converter for discord-formatted timestamp arguments. */ @Converter( - "timestamp", + "timestamp", - types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] + types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE] ) public class TimestampConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.timestamp.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.timestamp.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false - this.parsed = parseFromString(arg) ?: throw DiscordRelayedException( - context.translate( - "converters.timestamp.error.invalid", - replacements = arrayOf(arg) - ) - ) + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false + this.parsed = parseFromString(arg) ?: throw DiscordRelayedException( + context.translate( + "converters.timestamp.error.invalid", + replacements = arrayOf(arg) + ) + ) - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false - this.parsed = parseFromString(optionValue) ?: throw DiscordRelayedException( - context.translate( - "converters.timestamp.error.invalid", - replacements = arrayOf(optionValue) - ) - ) + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false + this.parsed = parseFromString(optionValue) ?: throw DiscordRelayedException( + context.translate( + "converters.timestamp.error.invalid", + replacements = arrayOf(optionValue) + ) + ) - return true - } + return true + } - internal companion object { - internal fun parseFromString(string: String): FormattedTimestamp? { - if (string.startsWith(TIMESTAMP_PREFIX) && string.endsWith(TIMESTAMP_SUFFIX)) { - val inner = string.removeSurrounding(TIMESTAMP_PREFIX, TIMESTAMP_SUFFIX).split(":") - val epochSeconds = inner.getOrNull(0) - val format = inner.getOrNull(1) + internal companion object { + internal fun parseFromString(string: String): FormattedTimestamp? { + if (string.startsWith(TIMESTAMP_PREFIX) && string.endsWith(TIMESTAMP_SUFFIX)) { + val inner = string.removeSurrounding(TIMESTAMP_PREFIX, TIMESTAMP_SUFFIX).split(":") + val epochSeconds = inner.getOrNull(0) + val format = inner.getOrNull(1) - return FormattedTimestamp( - Instant.fromEpochSeconds(epochSeconds?.toLongOrNull() ?: return null), - TimestampType.fromFormatSpecifier(format) ?: return null - ) - } else { - return null - } - } - } + return FormattedTimestamp( + Instant.fromEpochSeconds(epochSeconds?.toLongOrNull() ?: return null), + TimestampType.fromFormatSpecifier(format) ?: return null + ) + } else { + return null + } + } + } } /** @@ -92,8 +92,8 @@ public class TimestampConverter( * @param format Which format to display the timestamp in */ public data class FormattedTimestamp(val instant: Instant, val format: TimestampType) { - /** - * Format the timestamp using the format into Discord's special format. - */ - public fun toDiscord(): String = instant.toDiscord(format) + /** + * Format the timestamp using the format into Discord's special format. + */ + public fun toDiscord(): String = instant.toDiscord(format) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/UserConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/UserConverter.kt index 3fe62a9583..acb551605f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/UserConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/UserConverter.kt @@ -39,97 +39,97 @@ import kotlinx.coroutines.flow.firstOrNull * argument. */ @Converter( - "user", + "user", - types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], - builderFields = ["public var useReply: Boolean = true"] + types = [ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE], + builderFields = ["public var useReply: Boolean = true"] ) public class UserConverter( - private var useReply: Boolean = true, - override var validator: Validator = null, + private var useReply: Boolean = true, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.user.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.user.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - if (useReply && context is ChatCommandContext<*>) { - val messageReference = context.message.asMessage().messageReference + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + if (useReply && context is ChatCommandContext<*>) { + val messageReference = context.message.asMessage().messageReference - if (messageReference != null) { - val user = messageReference.message?.asMessage()?.author?.asUserOrNull() + if (messageReference != null) { + val user = messageReference.message?.asMessage()?.author?.asUserOrNull() - if (user != null) { - parsed = user - return true - } - } - } + if (user != null) { + parsed = user + return true + } + } + } - val arg: String = named ?: parser?.parseNext()?.data ?: return false + val arg: String = named ?: parser?.parseNext()?.data ?: return false - if (arg.equals("me", true)) { - val user = context.getUser()?.asUserOrNull() + if (arg.equals("me", true)) { + val user = context.getUser()?.asUserOrNull() - if (user != null) { - this.parsed = user + if (user != null) { + this.parsed = user - return true - } - } + return true + } + } - if (arg.equals("you", true)) { + if (arg.equals("you", true)) { this.parsed = bot.kordRef.getSelf() return true - } - - this.parsed = findUser(arg, context) - ?: throw DiscordRelayedException( - context.translate("converters.user.error.missing", replacements = arrayOf(arg)) - ) - - return true - } - - private suspend fun findUser(arg: String, context: CommandContext): User? = - if (arg.startsWith("<@") && arg.endsWith(">")) { // It's a mention - val id: String = arg.substring(2, arg.length - 1).replace("!", "") - - try { - kord.getUser(Snowflake(id)) - } catch (e: NumberFormatException) { - throw DiscordRelayedException( - context.translate("converters.user.error.invalid", replacements = arrayOf(id)) - ) - } - } else { - try { // Try for a user ID first - kord.getUser(Snowflake(arg)) - } catch (e: NumberFormatException) { // It's not an ID, let's try the tag - if (!arg.contains("#")) { - null - } else { - kord.users.firstOrNull { user -> - user.tag.equals(arg, true) - } - } - } - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - UserBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = if (context.eventObj is AutoCompleteInteractionCreateEvent) { - val id = (option as? UserOptionValue)?.value ?: return false - - kord.getUser(id) ?: return false - } else { - (option as? UserOptionValue)?.resolvedObject ?: return false - } - - this.parsed = optionValue - - return true - } + } + + this.parsed = findUser(arg, context) + ?: throw DiscordRelayedException( + context.translate("converters.user.error.missing", replacements = arrayOf(arg)) + ) + + return true + } + + private suspend fun findUser(arg: String, context: CommandContext): User? = + if (arg.startsWith("<@") && arg.endsWith(">")) { // It's a mention + val id: String = arg.substring(2, arg.length - 1).replace("!", "") + + try { + kord.getUser(Snowflake(id)) + } catch (e: NumberFormatException) { + throw DiscordRelayedException( + context.translate("converters.user.error.invalid", replacements = arrayOf(id)) + ) + } + } else { + try { // Try for a user ID first + kord.getUser(Snowflake(arg)) + } catch (e: NumberFormatException) { // It's not an ID, let's try the tag + if (!arg.contains("#")) { + null + } else { + kord.users.firstOrNull { user -> + user.tag.equals(arg, true) + } + } + } + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + UserBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = if (context.eventObj is AutoCompleteInteractionCreateEvent) { + val id = (option as? UserOptionValue)?.value ?: return false + + kord.getUser(id) ?: return false + } else { + (option as? UserOptionValue)?.resolvedObject ?: return false + } + + this.parsed = optionValue + + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/ChatCommandEvents.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/ChatCommandEvents.kt index bf8922768e..0b76b3aa6c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/ChatCommandEvents.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/ChatCommandEvents.kt @@ -12,33 +12,33 @@ import dev.kord.core.event.message.MessageCreateEvent /** Event emitted when a chat command is invoked. **/ public data class ChatCommandInvocationEvent( - override val command: ChatCommand<*>, - override val event: MessageCreateEvent + override val command: ChatCommand<*>, + override val event: MessageCreateEvent, ) : CommandInvocationEvent, MessageCreateEvent> /** Event emitted when a chat command invocation succeeds. **/ public data class ChatCommandSucceededEvent( - override val command: ChatCommand<*>, - override val event: MessageCreateEvent + override val command: ChatCommand<*>, + override val event: MessageCreateEvent, ) : CommandSucceededEvent, MessageCreateEvent> /** Event emitted when a chat command's checks fail. **/ public data class ChatCommandFailedChecksEvent( - override val command: ChatCommand<*>, - override val event: MessageCreateEvent, - override val reason: String, + override val command: ChatCommand<*>, + override val event: MessageCreateEvent, + override val reason: String, ) : CommandFailedChecksEvent, MessageCreateEvent> /** Event emitted when a chat command's argument parsing fails. **/ public data class ChatCommandFailedParsingEvent( - override val command: ChatCommand<*>, - override val event: MessageCreateEvent, - override val exception: ArgumentParsingException, + override val command: ChatCommand<*>, + override val event: MessageCreateEvent, + override val exception: ArgumentParsingException, ) : CommandFailedParsingEvent, MessageCreateEvent> /** Event emitted when a chat command's invocation fails with an exception. **/ public data class ChatCommandFailedWithExceptionEvent( - override val command: ChatCommand<*>, - override val event: MessageCreateEvent, - override val throwable: Throwable + override val command: ChatCommand<*>, + override val event: MessageCreateEvent, + override val throwable: Throwable, ) : CommandFailedWithExceptionEvent, MessageCreateEvent> diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/CommandEvents.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/CommandEvents.kt index b1774ec8ab..0427583ca2 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/CommandEvents.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/CommandEvents.kt @@ -18,11 +18,11 @@ import dev.kord.core.event.Event * @param E Event type */ public sealed interface CommandEvent : KordExEvent { - /** Command object that this event concerns. **/ - public val command: C + /** Command object that this event concerns. **/ + public val command: C - /** Event object that triggered this invocation. **/ - public val event: E + /** Event object that triggered this invocation. **/ + public val event: E } /** Basic event emitted when a command is invoked. **/ @@ -36,18 +36,18 @@ public sealed interface CommandFailedEvent : CommandEven /** Basic event emitted when a command's checks fail, including for required bot permissions. **/ public sealed interface CommandFailedChecksEvent : CommandFailedEvent { - /** Human-readable failure reason. **/ - public val reason: String + /** Human-readable failure reason. **/ + public val reason: String } /** Basic event emitted when a command's argument parsing fails. **/ public sealed interface CommandFailedParsingEvent : CommandFailedEvent { - /** Argument parsing exception object. **/ - public val exception: ArgumentParsingException + /** Argument parsing exception object. **/ + public val exception: ArgumentParsingException } /** Basic event emitted when a command's body invocation fails with an exception. **/ public sealed interface CommandFailedWithExceptionEvent : CommandFailedEvent { - /** Exception thrown for this failure. **/ - public val throwable: Throwable + /** Exception thrown for this failure. **/ + public val throwable: Throwable } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/MessageCommandEvents.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/MessageCommandEvents.kt index e2e39c6afa..78d6e15148 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/MessageCommandEvents.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/MessageCommandEvents.kt @@ -15,18 +15,18 @@ import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent /** Basic event emitted when a message command is invoked. **/ public interface MessageCommandInvocationEvent> : - CommandInvocationEvent + CommandInvocationEvent /** Event emitted when an ephemeral message command is invoked. **/ public data class EphemeralMessageCommandInvocationEvent( - override val command: EphemeralMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent + override val command: EphemeralMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, ) : MessageCommandInvocationEvent> /** Event emitted when a public message command is invoked. **/ public data class PublicMessageCommandInvocationEvent( - override val command: PublicMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent + override val command: PublicMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, ) : MessageCommandInvocationEvent> // endregion @@ -35,18 +35,18 @@ public data class PublicMessageCommandInvocationEvent( /** Basic event emitted when a message command invocation succeeds. **/ public interface MessageCommandSucceededEvent> : - CommandSucceededEvent + CommandSucceededEvent /** Event emitted when an ephemeral message command invocation succeeds. **/ public data class EphemeralMessageCommandSucceededEvent( - override val command: EphemeralMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent + override val command: EphemeralMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, ) : MessageCommandSucceededEvent> /** Event emitted when a public message command invocation succeeds. **/ public data class PublicMessageCommandSucceededEvent( - override val command: PublicMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent + override val command: PublicMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, ) : MessageCommandSucceededEvent> // endregion @@ -54,43 +54,43 @@ public data class PublicMessageCommandSucceededEvent( // region Failed events /** Basic event emitted when a message command invocation fails. **/ -public sealed interface MessageCommandFailedEvent> : - CommandFailedEvent +public sealed interface MessageCommandFailedEvent> : + CommandFailedEvent /** Basic event emitted when a message command's checks fail. **/ public interface MessageCommandFailedChecksEvent> : - MessageCommandFailedEvent, CommandFailedChecksEvent + MessageCommandFailedEvent, CommandFailedChecksEvent /** Event emitted when an ephemeral message command's checks fail. **/ public data class EphemeralMessageCommandFailedChecksEvent( - override val command: EphemeralMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent, - override val reason: String + override val command: EphemeralMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, + override val reason: String, ) : MessageCommandFailedChecksEvent> /** Event emitted when a public message command's checks fail. **/ public data class PublicMessageCommandFailedChecksEvent( - override val command: PublicMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent, - override val reason: String + override val command: PublicMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, + override val reason: String, ) : MessageCommandFailedChecksEvent> /** Basic event emitted when a message command invocation fails with an exception. **/ public interface MessageCommandFailedWithExceptionEvent> : - MessageCommandFailedEvent, CommandFailedWithExceptionEvent + MessageCommandFailedEvent, CommandFailedWithExceptionEvent /** Event emitted when an ephemeral message command invocation fails with an exception. **/ public data class EphemeralMessageCommandFailedWithExceptionEvent( - override val command: EphemeralMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: EphemeralMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, + override val throwable: Throwable, ) : MessageCommandFailedWithExceptionEvent> /** Event emitted when a public message command invocation fails with an exception. **/ public data class PublicMessageCommandFailedWithExceptionEvent( - override val command: PublicMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: PublicMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, + override val throwable: Throwable, ) : MessageCommandFailedWithExceptionEvent> // endregion diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/SlashCommandEvents.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/SlashCommandEvents.kt index 6681395ca1..ba59a94bb0 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/SlashCommandEvents.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/SlashCommandEvents.kt @@ -16,18 +16,18 @@ import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent /** Basic event emitted when a slash command is invoked. **/ public interface SlashCommandInvocationEvent> : - CommandInvocationEvent + CommandInvocationEvent /** Event emitted when an ephemeral slash command is invoked. **/ public data class EphemeralSlashCommandInvocationEvent( - override val command: EphemeralSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent + override val command: EphemeralSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, ) : SlashCommandInvocationEvent> /** Event emitted when a public slash command is invoked. **/ public data class PublicSlashCommandInvocationEvent( - override val command: PublicSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent + override val command: PublicSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, ) : SlashCommandInvocationEvent> // endregion @@ -36,18 +36,18 @@ public data class PublicSlashCommandInvocationEvent( /** Basic event emitted when a slash command invocation succeeds. **/ public interface SlashCommandSucceededEvent> : - CommandSucceededEvent + CommandSucceededEvent /** Event emitted when an ephemeral slash command invocation succeeds. **/ public data class EphemeralSlashCommandSucceededEvent( - override val command: EphemeralSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent + override val command: EphemeralSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, ) : SlashCommandSucceededEvent> /** Event emitted when a public slash command invocation succeeds. **/ public data class PublicSlashCommandSucceededEvent( - override val command: PublicSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent + override val command: PublicSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, ) : SlashCommandSucceededEvent> // endregion @@ -56,60 +56,60 @@ public data class PublicSlashCommandSucceededEvent( /** Basic event emitted when a slash command invocation fails. **/ public sealed interface SlashCommandFailedEvent> : - CommandFailedEvent + CommandFailedEvent /** Basic event emitted when a slash command's checks fail. **/ public interface SlashCommandFailedChecksEvent> : - SlashCommandFailedEvent, CommandFailedChecksEvent + SlashCommandFailedEvent, CommandFailedChecksEvent /** Event emitted when an ephemeral slash command's checks fail. **/ public data class EphemeralSlashCommandFailedChecksEvent( - override val command: EphemeralSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val reason: String, + override val command: EphemeralSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val reason: String, ) : SlashCommandFailedChecksEvent> /** Event emitted when a public slash command's checks fail. **/ public data class PublicSlashCommandFailedChecksEvent( - override val command: PublicSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val reason: String, + override val command: PublicSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val reason: String, ) : SlashCommandFailedChecksEvent> /** Basic event emitted when a slash command's argument parsing fails'. **/ public interface SlashCommandFailedParsingEvent> : - SlashCommandFailedEvent, CommandFailedParsingEvent + SlashCommandFailedEvent, CommandFailedParsingEvent /** Event emitted when an ephemeral slash command's argument parsing fails'. **/ public data class EphemeralSlashCommandFailedParsingEvent( - override val command: EphemeralSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val exception: ArgumentParsingException, + override val command: EphemeralSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val exception: ArgumentParsingException, ) : SlashCommandFailedParsingEvent> /** Event emitted when a public slash command's argument parsing fails'. **/ public data class PublicSlashCommandFailedParsingEvent( - override val command: PublicSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val exception: ArgumentParsingException, + override val command: PublicSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val exception: ArgumentParsingException, ) : SlashCommandFailedParsingEvent> /** Basic event emitted when a slash command invocation fails with an exception. **/ public interface SlashCommandFailedWithExceptionEvent> : - SlashCommandFailedEvent, CommandFailedWithExceptionEvent + SlashCommandFailedEvent, CommandFailedWithExceptionEvent /** Event emitted when an ephemeral slash command invocation fails with an exception. **/ public data class EphemeralSlashCommandFailedWithExceptionEvent( - override val command: EphemeralSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: EphemeralSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val throwable: Throwable, ) : SlashCommandFailedWithExceptionEvent> /** Event emitted when a public slash command invocation fails with an exception. **/ public data class PublicSlashCommandFailedWithExceptionEvent( - override val command: PublicSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: PublicSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val throwable: Throwable, ) : SlashCommandFailedWithExceptionEvent> // endregion diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/UserCommandEvents.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/UserCommandEvents.kt index 1bf3ee51ea..14b0eb0548 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/UserCommandEvents.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/events/UserCommandEvents.kt @@ -15,18 +15,18 @@ import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent /** Basic event emitted when a user command is invoked. **/ public interface UserCommandInvocationEvent> : - CommandInvocationEvent + CommandInvocationEvent /** Event emitted when an ephemeral user command is invoked. **/ public data class EphemeralUserCommandInvocationEvent( - override val command: EphemeralUserCommand<*>, - override val event: UserCommandInteractionCreateEvent + override val command: EphemeralUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, ) : UserCommandInvocationEvent> /** Event emitted when a public user command is invoked. **/ public data class PublicUserCommandInvocationEvent( - override val command: PublicUserCommand<*>, - override val event: UserCommandInteractionCreateEvent + override val command: PublicUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, ) : UserCommandInvocationEvent> // endregion @@ -35,18 +35,18 @@ public data class PublicUserCommandInvocationEvent( /** Basic event emitted when a user command invocation succeeds. **/ public interface UserCommandSucceededEvent> : - CommandSucceededEvent + CommandSucceededEvent /** Event emitted when an ephemeral user command invocation succeeds. **/ public data class EphemeralUserCommandSucceededEvent( - override val command: EphemeralUserCommand<*>, - override val event: UserCommandInteractionCreateEvent + override val command: EphemeralUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, ) : UserCommandSucceededEvent> /** Event emitted when a public user command invocation succeeds. **/ public data class PublicUserCommandSucceededEvent( - override val command: PublicUserCommand<*>, - override val event: UserCommandInteractionCreateEvent + override val command: PublicUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, ) : UserCommandSucceededEvent> // endregion @@ -55,42 +55,42 @@ public data class PublicUserCommandSucceededEvent( /** Basic event emitted when a user command invocation fails. **/ public sealed interface UserCommandFailedEvent> : - CommandFailedEvent + CommandFailedEvent /** Basic event emitted when a user command's checks fail. **/ public interface UserCommandFailedChecksEvent> : - UserCommandFailedEvent, CommandFailedChecksEvent + UserCommandFailedEvent, CommandFailedChecksEvent /** Event emitted when an ephemeral user command's checks fail. **/ public data class EphemeralUserCommandFailedChecksEvent( - override val command: EphemeralUserCommand<*>, - override val event: UserCommandInteractionCreateEvent, - override val reason: String, + override val command: EphemeralUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, + override val reason: String, ) : UserCommandFailedChecksEvent> /** Event emitted when a public user command's checks fail. **/ public data class PublicUserCommandFailedChecksEvent( - override val command: PublicUserCommand<*>, - override val event: UserCommandInteractionCreateEvent, - override val reason: String, + override val command: PublicUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, + override val reason: String, ) : UserCommandFailedChecksEvent> /** Basic event emitted when a user command invocation fails with an exception. **/ public interface UserCommandFailedWithExceptionEvent> : - UserCommandFailedEvent, CommandFailedWithExceptionEvent + UserCommandFailedEvent, CommandFailedWithExceptionEvent /** Event emitted when an ephemeral user command invocation fails with an exception. **/ public data class EphemeralUserCommandFailedWithExceptionEvent( - override val command: EphemeralUserCommand<*>, - override val event: UserCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: EphemeralUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, + override val throwable: Throwable, ) : UserCommandFailedWithExceptionEvent> /** Event emitted when a public user command invocation fails with an exception. **/ public data class PublicUserCommandFailedWithExceptionEvent( - override val command: PublicUserCommand<*>, - override val event: UserCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: PublicUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, + override val throwable: Throwable, ) : UserCommandFailedWithExceptionEvent> // endregion diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/Component.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/Component.kt index 6c79674dab..422187a80d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/Component.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/Component.kt @@ -18,33 +18,33 @@ import org.koin.core.component.inject /** Abstract class representing a basic Discord component. **/ public abstract class Component : KordExKoinComponent { - /** Component width, how many "slots" in one row it needs to be added to the row. **/ - public open val unitWidth: Int = 1 + /** Component width, how many "slots" in one row it needs to be added to the row. **/ + public open val unitWidth: Int = 1 - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by inject() + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by inject() - /** Quick access to the command registry. **/ - public val registry: ApplicationCommandRegistry by inject() + /** Quick access to the command registry. **/ + public val registry: ApplicationCommandRegistry by inject() - /** Bot settings object. **/ - public val settings: ExtensibleBotBuilder by inject() + /** Bot settings object. **/ + public val settings: ExtensibleBotBuilder by inject() - /** Bot object. **/ - public val bot: ExtensibleBot by inject() + /** Bot object. **/ + public val bot: ExtensibleBot by inject() - /** Kord instance, backing the ExtensibleBot. **/ - public val kord: Kord by inject() + /** Kord instance, backing the ExtensibleBot. **/ + public val kord: Kord by inject() - /** Sentry adapter, for easy access to Sentry functions. **/ - public val sentry: SentryAdapter by inject() + /** Sentry adapter, for easy access to Sentry functions. **/ + public val sentry: SentryAdapter by inject() - /** Translation bundle, to retrieve translations from. **/ - public open var bundle: String? = null + /** Translation bundle, to retrieve translations from. **/ + public open var bundle: String? = null - /** Validation function, called to ensure the component is valid, throws exceptions if not. **/ - public abstract fun validate() + /** Validation function, called to ensure the component is valid, throws exceptions if not. **/ + public abstract fun validate() - /** Called to apply the given component to a Kord [ActionRowBuilder]. **/ - public abstract fun apply(builder: ActionRowBuilder) + /** Called to apply the given component to a Kord [ActionRowBuilder]. **/ + public abstract fun apply(builder: ActionRowBuilder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentContext.kt index ab753d7cf7..e153dd3008 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentContext.kt @@ -39,136 +39,136 @@ import java.util.* * @param cache Data cache map shared with the defined checks. */ public abstract class ComponentContext( - public open val component: Component, - public open val event: E, - public open val cache: MutableStringKeyedMap, + public open val component: Component, + public open val event: E, + public open val cache: MutableStringKeyedMap, ) : KordExKoinComponent, TranslatableContext { - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by inject() - - override val bundle: String? - get() = component.bundle - - /** Configured bot settings object. **/ - public val settings: ExtensibleBotBuilder by inject() - - /** Current Sentry context, containing breadcrumbs and other goodies. **/ - public val sentry: SentryContext = SentryContext() - - /** Cached locale variable, stored and retrieved by [getLocale]. **/ - public override var resolvedLocale: Locale? = null - - /** Channel this component was interacted with within. **/ - public open lateinit var channel: MessageChannelBehavior - - /** Guild this component was interacted with within, if any. **/ - public open var guild: GuildBehavior? = null - - /** Member that interacted with this component, if on a guild. **/ - public open var member: MemberBehavior? = null - - /** The message the component is attached to. **/ - public open lateinit var message: Message - - /** User that interacted with this component. **/ - public open lateinit var user: UserBehavior - - /** Called before processing, used to populate any extra variables from event data. **/ - public open suspend fun populate() { - channel = getChannel() - guild = getGuild() - member = getMember() - message = getMessage() - user = getUser() - } - - /** Extract channel information from event data, if that context is available. **/ - @JvmName("getChannel1") - public fun getChannel(): MessageChannelBehavior = - event.interaction.channel - - /** Extract guild information from event data, if that context is available. **/ - @OptIn(KordUnsafe::class, KordExperimental::class) - @JvmName("getGuild1") - public fun getGuild(): GuildBehavior? = - event.interaction.data.guildId.value?.let { event.kord.unsafe.guild(it) } - - /** Extract member information from event data, if that context is available. **/ - @JvmName("getMember1") - public fun getMember(): MemberBehavior? = - getGuild()?.let { Member(event.interaction.data.member.value!!, event.interaction.user.data, event.kord) } - - /** Extract message information from event data, if that context is available. **/ - @JvmName("getMessage1") - public fun getMessage(): Message = - event.interaction.message - - /** Extract user information from event data, if that context is available. **/ - @JvmName("getUser1") - public fun getUser(): UserBehavior = - event.interaction.user - - /** Resolve the locale for this command context. **/ - public override suspend fun getLocale(): Locale { - var locale: Locale? = resolvedLocale - - if (locale != null) { - return locale - } - - val guild = guildFor(event) - val channel = channelFor(event) - val user = userFor(event) - - for (resolver in settings.i18nBuilder.localeResolvers) { - val result = resolver(guild, channel, user, event.interaction) - - if (result != null) { - locale = result - break - } - } - - resolvedLocale = locale ?: settings.i18nBuilder.defaultLocale - - return resolvedLocale!! - } - - /** - * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured - * locale resolvers. - */ - public override suspend fun translate( - key: String, - bundleName: String?, - replacements: Array, - ): String { - val locale = getLocale() - - return translationsProvider.translate(key, locale, bundleName, replacements) - } - - /** - * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured - * locale resolvers. - */ - public override suspend fun translate( - key: String, - bundleName: String?, - replacements: Map, - ): String { - val locale = getLocale() - - return translationsProvider.translate(key, locale, bundleName, replacements) - } - - /** - * @param capture breadcrumb data will be modified to add the component context information - */ - public suspend fun addContextDataToBreadcrumb(capture: SentryBreadcrumbCapture) { + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by inject() + + override val bundle: String? + get() = component.bundle + + /** Configured bot settings object. **/ + public val settings: ExtensibleBotBuilder by inject() + + /** Current Sentry context, containing breadcrumbs and other goodies. **/ + public val sentry: SentryContext = SentryContext() + + /** Cached locale variable, stored and retrieved by [getLocale]. **/ + public override var resolvedLocale: Locale? = null + + /** Channel this component was interacted with within. **/ + public open lateinit var channel: MessageChannelBehavior + + /** Guild this component was interacted with within, if any. **/ + public open var guild: GuildBehavior? = null + + /** Member that interacted with this component, if on a guild. **/ + public open var member: MemberBehavior? = null + + /** The message the component is attached to. **/ + public open lateinit var message: Message + + /** User that interacted with this component. **/ + public open lateinit var user: UserBehavior + + /** Called before processing, used to populate any extra variables from event data. **/ + public open suspend fun populate() { + channel = getChannel() + guild = getGuild() + member = getMember() + message = getMessage() + user = getUser() + } + + /** Extract channel information from event data, if that context is available. **/ + @JvmName("getChannel1") + public fun getChannel(): MessageChannelBehavior = + event.interaction.channel + + /** Extract guild information from event data, if that context is available. **/ + @OptIn(KordUnsafe::class, KordExperimental::class) + @JvmName("getGuild1") + public fun getGuild(): GuildBehavior? = + event.interaction.data.guildId.value?.let { event.kord.unsafe.guild(it) } + + /** Extract member information from event data, if that context is available. **/ + @JvmName("getMember1") + public fun getMember(): MemberBehavior? = + getGuild()?.let { Member(event.interaction.data.member.value!!, event.interaction.user.data, event.kord) } + + /** Extract message information from event data, if that context is available. **/ + @JvmName("getMessage1") + public fun getMessage(): Message = + event.interaction.message + + /** Extract user information from event data, if that context is available. **/ + @JvmName("getUser1") + public fun getUser(): UserBehavior = + event.interaction.user + + /** Resolve the locale for this command context. **/ + public override suspend fun getLocale(): Locale { + var locale: Locale? = resolvedLocale + + if (locale != null) { + return locale + } + + val guild = guildFor(event) + val channel = channelFor(event) + val user = userFor(event) + + for (resolver in settings.i18nBuilder.localeResolvers) { + val result = resolver(guild, channel, user, event.interaction) + + if (result != null) { + locale = result + break + } + } + + resolvedLocale = locale ?: settings.i18nBuilder.defaultLocale + + return resolvedLocale!! + } + + /** + * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured + * locale resolvers. + */ + public override suspend fun translate( + key: String, + bundleName: String?, + replacements: Array, + ): String { + val locale = getLocale() + + return translationsProvider.translate(key, locale, bundleName, replacements) + } + + /** + * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured + * locale resolvers. + */ + public override suspend fun translate( + key: String, + bundleName: String?, + replacements: Map, + ): String { + val locale = getLocale() + + return translationsProvider.translate(key, locale, bundleName, replacements) + } + + /** + * @param capture breadcrumb data will be modified to add the component context information + */ + public suspend fun addContextDataToBreadcrumb(capture: SentryBreadcrumbCapture) { capture.channel = channel.asChannelOrNull() capture.guild = guild?.asGuildOrNull() - capture.data["message.id"] = message.id.toString() - } + capture.data["message.id"] = message.id.toString() + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentRegistry.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentRegistry.kt index e1b3d85e70..8e6e4d51a7 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentRegistry.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentRegistry.kt @@ -23,87 +23,87 @@ import io.github.oshai.kotlinlogging.KotlinLogging * registered component actions. */ public open class ComponentRegistry { - internal val logger: KLogger = KotlinLogging.logger {} + internal val logger: KLogger = KotlinLogging.logger {} - /** Scheduler that can be used to schedule timeout tasks. All [ComponentContainer] objects use this scheduler. **/ - public open val scheduler: Scheduler = Scheduler() + /** Scheduler that can be used to schedule timeout tasks. All [ComponentContainer] objects use this scheduler. **/ + public open val scheduler: Scheduler = Scheduler() - /** Mapping of registered component IDs to their components. **/ - public open val storage: RegistryStorage = DefaultLocalRegistryStorage() + /** Mapping of registered component IDs to their components. **/ + public open val storage: RegistryStorage = DefaultLocalRegistryStorage() - /** Mapping of registered modal IDs to their form objects. **/ - public open val modalStorage: RegistryStorage = DefaultLocalRegistryStorage() + /** Mapping of registered modal IDs to their form objects. **/ + public open val modalStorage: RegistryStorage = DefaultLocalRegistryStorage() - /** Register a component. Only components with IDs need registering. **/ - public open suspend fun register(component: ComponentWithID) { - logger.trace { "Registering component with ID: ${component.id}" } + /** Register a component. Only components with IDs need registering. **/ + public open suspend fun register(component: ComponentWithID) { + logger.trace { "Registering component with ID: ${component.id}" } - storage.set(component.id, component) - } + storage.set(component.id, component) + } - /** Register a modal form. **/ - public open suspend fun register(modal: ModalForm) { - logger.trace { "Registering modal with ID: ${modal.id}" } + /** Register a modal form. **/ + public open suspend fun register(modal: ModalForm) { + logger.trace { "Registering modal with ID: ${modal.id}" } - modalStorage.set(modal.id, modal) - } + modalStorage.set(modal.id, modal) + } - /** Unregister a registered component. **/ - public open suspend fun unregister(component: ComponentWithID): Component? = - unregister(component.id) + /** Unregister a registered component. **/ + public open suspend fun unregister(component: ComponentWithID): Component? = + unregister(component.id) - /** Unregister a registered modal form. **/ - public open suspend fun unregisterModal(modal: ModalForm): ModalForm? = - unregisterModal(modal.id) + /** Unregister a registered modal form. **/ + public open suspend fun unregisterModal(modal: ModalForm): ModalForm? = + unregisterModal(modal.id) - /** Unregister a registered component, by ID. **/ - public open suspend fun unregister(id: String): Component? = - storage.remove(id) + /** Unregister a registered component, by ID. **/ + public open suspend fun unregister(id: String): Component? = + storage.remove(id) - /** Unregister a registered modal form, by ID. **/ - public open suspend fun unregisterModal(id: String): ModalForm? = - modalStorage.remove(id) + /** Unregister a registered modal form, by ID. **/ + public open suspend fun unregisterModal(id: String): ModalForm? = + modalStorage.remove(id) - /** Dispatch a [ModalSubmitInteractionCreateEvent] to its modal form object. **/ - public suspend fun handle(event: ModalSubmitInteractionCreateEvent) { - val id = event.interaction.modalId - val modal = modalStorage.get(id) + /** Dispatch a [ModalSubmitInteractionCreateEvent] to its modal form object. **/ + public suspend fun handle(event: ModalSubmitInteractionCreateEvent) { + val id = event.interaction.modalId + val modal = modalStorage.get(id) - if (modal == null) { - logger.debug { "Modal interaction received for unknown modal ID: $id" } - } else { - modal.call(event) - } - } + if (modal == null) { + logger.debug { "Modal interaction received for unknown modal ID: $id" } + } else { + modal.call(event) + } + } - /** Dispatch a [ButtonInteractionCreateEvent] to its button component object. **/ - public suspend fun handle(event: ButtonInteractionCreateEvent) { - val id = event.interaction.componentId + /** Dispatch a [ButtonInteractionCreateEvent] to its button component object. **/ + public suspend fun handle(event: ButtonInteractionCreateEvent) { + val id = event.interaction.componentId - when (val c = storage.get(id)) { - is InteractionButtonWithAction<*, *> -> c.call(event) + when (val c = storage.get(id)) { + is InteractionButtonWithAction<*, *> -> c.call(event) - null -> logger.debug { "Button interaction received for unknown component ID: $id" } + null -> logger.debug { "Button interaction received for unknown component ID: $id" } - else -> logger.warn { - "Button interaction received for component ($id), but that component is not a button component with " + - "an action" - } - } - } + else -> logger.warn { + "Button interaction received for component ($id), but that component is not a button component with " + + "an action" + } + } + } - /** Dispatch a [SelectMenuInteractionCreateEvent] to its select (dropdown) menu component object. **/ - public suspend fun handle(event: SelectMenuInteractionCreateEvent) { - val id = event.interaction.componentId + /** Dispatch a [SelectMenuInteractionCreateEvent] to its select (dropdown) menu component object. **/ + public suspend fun handle(event: SelectMenuInteractionCreateEvent) { + val id = event.interaction.componentId - when (val c = storage.get(id)) { - is SelectMenu<*, *> -> c.call(event) + when (val c = storage.get(id)) { + is SelectMenu<*, *> -> c.call(event) - null -> logger.debug { "Select Menu interaction received for unknown component ID: $id" } + null -> logger.debug { "Select Menu interaction received for unknown component ID: $id" } - else -> logger.warn { - "Select Menu interaction received for component ($id), but that component is not a select menu" - } - } - } + else -> logger.warn { + "Select Menu interaction received for component ($id), but that component is not a select menu" + } + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentWithAction.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentWithAction.kt index 7ce93d7375..7564ae1344 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentWithAction.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentWithAction.kt @@ -38,139 +38,139 @@ import org.koin.core.component.inject * @param modal Callback returning a ModalForm object, probably the constructor of a subtype. */ public abstract class ComponentWithAction< - E : ComponentInteractionCreateEvent, - C : ComponentContext<*>, - M : ModalForm, - >( - public open val timeoutTask: Task?, - public open val modal: (() -> M)? = null, + E : ComponentInteractionCreateEvent, + C : ComponentContext<*>, + M : ModalForm, + >( + public open val timeoutTask: Task?, + public open val modal: (() -> M)? = null, ) : ComponentWithID(), Lockable { - private val logger: KLogger = KotlinLogging.logger {} - - /** @suppress This is only meant for use by code that extends the command system. **/ - public val componentRegistry: ComponentRegistry by inject() - - /** Whether to use a deferred ack, which will prevent Discord's "Thinking..." message. **/ - public open var deferredAck: Boolean = true - - /** @suppress **/ - public open val checkList: MutableList> = mutableListOf() - - /** Bot permissions required to be able to run execute this component's action. **/ - public open val requiredPerms: MutableSet = mutableSetOf() - - public override var locking: Boolean = false - - override var mutex: Mutex? = null - - /** Component body, to be called when the component is interacted with. **/ - public lateinit var body: suspend C.(M?) -> Unit - - /** Call this to supply a component [body], to be called when the component is interacted with. **/ - public fun action(action: suspend C.(M?) -> Unit) { - body = action - } - - /** - * Define a check which must pass for the component's body to be executed. - * - * A component may have multiple checks - all checks must pass for the component's body to be executed. - * Checks will be run in the order that they're defined. - * - * This function can be used DSL-style with a given body, or it can be passed one or more - * predefined functions. See the samples for more information. - * - * @param checks Checks to apply to this command. - */ - public open fun check(vararg checks: CheckWithCache) { - checks.forEach { checkList.add(it) } - } - - /** - * Overloaded check function to allow for DSL syntax. - * - * @param check Check to apply to this command. - */ - public open fun check(check: CheckWithCache) { - checkList.add(check) - } - - override fun validate() { - super.validate() - - if (!::body.isInitialized) { - error("No component body given.") - } - - if (locking && mutex == null) { - mutex = Mutex() - } - } - - /** If your bot requires permissions to be able to execute this component's body, add them using this function. **/ - public fun requireBotPermissions(vararg perms: Permission) { - perms.forEach(requiredPerms::add) - } - - /** Runs standard checks that can be handled in a generic way, without worrying about subclass-specific checks. **/ - @Throws(DiscordRelayedException::class) - public open suspend fun runStandardChecks(event: E, cache: MutableStringKeyedMap): Boolean { - val locale = event.getLocale() - - checkList.forEach { check -> - val context = CheckContextWithCache(event, locale, cache) - - check(context) - - if (!context.passed) { - context.throwIfFailedWithMessage() - - return false - } - } - - return true - } - - /** Override this in order to implement any subclass-specific checks. **/ - @Throws(DiscordRelayedException::class) - public open suspend fun runChecks(event: E, cache: MutableStringKeyedMap): Boolean = - runStandardChecks(event, cache) - - /** Checks whether the bot has the specified required permissions, throwing if it doesn't. **/ - @Throws(DiscordRelayedException::class) - public open suspend fun checkBotPerms(context: C) { - if (requiredPerms.isEmpty()) { - return // Nothing to check, don't try to hit the cache - } - - if (context.guild != null) { - val perms = context - .getChannel() - .asChannelOf() - .permissionsForMember(kord.selfId) - - val missingPerms = requiredPerms.filter { !perms.contains(it) } - - if (missingPerms.isNotEmpty()) { - throw DiscordRelayedException( - context.translate( - "commands.error.missingBotPermissions", - null, - - replacements = arrayOf( - missingPerms - .map { it.translate(context.getLocale()) } - .joinToString() - ) - ) - ) - } - } - } - - /** Override this to implement your component's calling logic. Check subtypes for examples! **/ - public open suspend fun call(event: E) { - timeoutTask?.restart() - } + private val logger: KLogger = KotlinLogging.logger {} + + /** @suppress This is only meant for use by code that extends the command system. **/ + public val componentRegistry: ComponentRegistry by inject() + + /** Whether to use a deferred ack, which will prevent Discord's "Thinking..." message. **/ + public open var deferredAck: Boolean = true + + /** @suppress **/ + public open val checkList: MutableList> = mutableListOf() + + /** Bot permissions required to be able to run execute this component's action. **/ + public open val requiredPerms: MutableSet = mutableSetOf() + + public override var locking: Boolean = false + + override var mutex: Mutex? = null + + /** Component body, to be called when the component is interacted with. **/ + public lateinit var body: suspend C.(M?) -> Unit + + /** Call this to supply a component [body], to be called when the component is interacted with. **/ + public fun action(action: suspend C.(M?) -> Unit) { + body = action + } + + /** + * Define a check which must pass for the component's body to be executed. + * + * A component may have multiple checks - all checks must pass for the component's body to be executed. + * Checks will be run in the order that they're defined. + * + * This function can be used DSL-style with a given body, or it can be passed one or more + * predefined functions. See the samples for more information. + * + * @param checks Checks to apply to this command. + */ + public open fun check(vararg checks: CheckWithCache) { + checks.forEach { checkList.add(it) } + } + + /** + * Overloaded check function to allow for DSL syntax. + * + * @param check Check to apply to this command. + */ + public open fun check(check: CheckWithCache) { + checkList.add(check) + } + + override fun validate() { + super.validate() + + if (!::body.isInitialized) { + error("No component body given.") + } + + if (locking && mutex == null) { + mutex = Mutex() + } + } + + /** If your bot requires permissions to be able to execute this component's body, add them using this function. **/ + public fun requireBotPermissions(vararg perms: Permission) { + perms.forEach(requiredPerms::add) + } + + /** Runs standard checks that can be handled in a generic way, without worrying about subclass-specific checks. **/ + @Throws(DiscordRelayedException::class) + public open suspend fun runStandardChecks(event: E, cache: MutableStringKeyedMap): Boolean { + val locale = event.getLocale() + + checkList.forEach { check -> + val context = CheckContextWithCache(event, locale, cache) + + check(context) + + if (!context.passed) { + context.throwIfFailedWithMessage() + + return false + } + } + + return true + } + + /** Override this in order to implement any subclass-specific checks. **/ + @Throws(DiscordRelayedException::class) + public open suspend fun runChecks(event: E, cache: MutableStringKeyedMap): Boolean = + runStandardChecks(event, cache) + + /** Checks whether the bot has the specified required permissions, throwing if it doesn't. **/ + @Throws(DiscordRelayedException::class) + public open suspend fun checkBotPerms(context: C) { + if (requiredPerms.isEmpty()) { + return // Nothing to check, don't try to hit the cache + } + + if (context.guild != null) { + val perms = context + .getChannel() + .asChannelOf() + .permissionsForMember(kord.selfId) + + val missingPerms = requiredPerms.filter { !perms.contains(it) } + + if (missingPerms.isNotEmpty()) { + throw DiscordRelayedException( + context.translate( + "commands.error.missingBotPermissions", + null, + + replacements = arrayOf( + missingPerms + .map { it.translate(context.getLocale()) } + .joinToString() + ) + ) + ) + } + } + } + + /** Override this to implement your component's calling logic. Check subtypes for examples! **/ + public open suspend fun call(event: E) { + timeoutTask?.restart() + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentWithID.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentWithID.kt index 49fe31ce2b..92817a6e15 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentWithID.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/ComponentWithID.kt @@ -10,12 +10,12 @@ import java.util.* /** Abstract class representing a component with an ID, which defaults to a newly-generated UUID. **/ public abstract class ComponentWithID : Component() { - /** Component's ID, a UUID by default. **/ - public open var id: String = UUID.randomUUID().toString() + /** Component's ID, a UUID by default. **/ + public open var id: String = UUID.randomUUID().toString() - public override fun validate() { - if (id.isEmpty()) { - error("All components must have a unique ID.") - } - } + public override fun validate() { + if (id.isEmpty()) { + error("All components must have a unique ID.") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/DisabledInteractionButton.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/DisabledInteractionButton.kt index 15b2563e57..6e95955897 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/DisabledInteractionButton.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/DisabledInteractionButton.kt @@ -11,23 +11,23 @@ import dev.kord.rest.builder.component.ActionRowBuilder /** Class representing a disabled button component, which has no action. **/ public open class DisabledInteractionButton : InteractionButtonWithID() { - /** Button style - anything but Link is valid. **/ - public open var style: ButtonStyle = ButtonStyle.Primary + /** Button style - anything but Link is valid. **/ + public open var style: ButtonStyle = ButtonStyle.Primary - override fun apply(builder: ActionRowBuilder) { - builder.interactionButton(style, id) { - emoji = partialEmoji - label = this@DisabledInteractionButton.label + override fun apply(builder: ActionRowBuilder) { + builder.interactionButton(style, id) { + emoji = partialEmoji + label = this@DisabledInteractionButton.label - disabled = true - } - } + disabled = true + } + } - override fun validate() { - super.validate() + override fun validate() { + super.validate() - if (style == ButtonStyle.Link) { - error("The Link button style is reserved for link buttons.") - } - } + if (style == ButtonStyle.Link) { + error("The Link button style is reserved for link buttons.") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/EphemeralInteractionButton.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/EphemeralInteractionButton.kt index 43385c6414..bc33e7e41b 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/EphemeralInteractionButton.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/EphemeralInteractionButton.kt @@ -24,123 +24,123 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialEphemeralButtonResponseBuilder = - (suspend InteractionResponseCreateBuilder.(ButtonInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(ButtonInteractionCreateEvent) -> Unit)? /** Class representing an ephemeral-only interaction button. **/ public open class EphemeralInteractionButton( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : InteractionButtonWithAction, M>(timeoutTask) { - /** Button style - anything but Link is valid. **/ - public open var style: ButtonStyle = ButtonStyle.Primary - - /** @suppress Initial response builder. **/ - public open var initialResponseBuilder: InitialEphemeralButtonResponseBuilder = null - - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialEphemeralButtonResponseBuilder) { - initialResponseBuilder = body - } - - override fun apply(builder: ActionRowBuilder) { - builder.interactionButton(style, id) { - emoji = partialEmoji - label = this@EphemeralInteractionButton.label - - disabled = this@EphemeralInteractionButton.disabled - } - } - - override suspend fun call(event: ButtonInteractionCreateEvent): Unit = withLock { - val cache: MutableStringKeyedMap = mutableMapOf() - - super.call(event) - - try { - if (!runChecks(event, cache)) { - return@withLock - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - return@withLock - } - - val modalObj = modal?.invoke() - - val response = if (initialResponseBuilder != null) { - event.interaction.respondEphemeral { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) - - val locale = event.getLocale() - - event.interaction.modal( - modalObj.translateTitle(locale, bundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), bundle) - } - - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) - - if (!deferredAck) { - it?.deferEphemeralResponseUnsafe() - } else { - it?.deferEphemeralMessageUpdate() - } - } ?: return@withLock - } else { - if (!deferredAck) { - event.interaction.deferEphemeralResponseUnsafe() - } else { - event.interaction.deferEphemeralMessageUpdate() - } - } - - val context = EphemeralInteractionButtonContext(this, event, response, cache) - - context.populate() - - firstSentryBreadcrumb(context, this) - - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - - return@withLock - } - - try { - body(context, modalObj) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.RelayedFailure(e)) - } catch (t: Throwable) { - handleError(context, t, this) - } - } - - override fun validate() { - super.validate() - - if (modal != null && initialResponseBuilder != null) { - error("You may not provide a modal builder and an initial response - pick one, not both.") - } - - if (style == ButtonStyle.Link) { - error("The Link button style is reserved for link buttons.") - } - } - - override suspend fun respondText( - context: EphemeralInteractionButtonContext, - message: String, - failureType: FailureReason<*> - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + /** Button style - anything but Link is valid. **/ + public open var style: ButtonStyle = ButtonStyle.Primary + + /** @suppress Initial response builder. **/ + public open var initialResponseBuilder: InitialEphemeralButtonResponseBuilder = null + + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialEphemeralButtonResponseBuilder) { + initialResponseBuilder = body + } + + override fun apply(builder: ActionRowBuilder) { + builder.interactionButton(style, id) { + emoji = partialEmoji + label = this@EphemeralInteractionButton.label + + disabled = this@EphemeralInteractionButton.disabled + } + } + + override suspend fun call(event: ButtonInteractionCreateEvent): Unit = withLock { + val cache: MutableStringKeyedMap = mutableMapOf() + + super.call(event) + + try { + if (!runChecks(event, cache)) { + return@withLock + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + return@withLock + } + + val modalObj = modal?.invoke() + + val response = if (initialResponseBuilder != null) { + event.interaction.respondEphemeral { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) + + val locale = event.getLocale() + + event.interaction.modal( + modalObj.translateTitle(locale, bundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), bundle) + } + + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) + + if (!deferredAck) { + it?.deferEphemeralResponseUnsafe() + } else { + it?.deferEphemeralMessageUpdate() + } + } ?: return@withLock + } else { + if (!deferredAck) { + event.interaction.deferEphemeralResponseUnsafe() + } else { + event.interaction.deferEphemeralMessageUpdate() + } + } + + val context = EphemeralInteractionButtonContext(this, event, response, cache) + + context.populate() + + firstSentryBreadcrumb(context, this) + + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + + return@withLock + } + + try { + body(context, modalObj) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.RelayedFailure(e)) + } catch (t: Throwable) { + handleError(context, t, this) + } + } + + override fun validate() { + super.validate() + + if (modal != null && initialResponseBuilder != null) { + error("You may not provide a modal builder and an initial response - pick one, not both.") + } + + if (style == ButtonStyle.Link) { + error("The Link button style is reserved for link buttons.") + } + } + + override suspend fun respondText( + context: EphemeralInteractionButtonContext, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/EphemeralInteractionButtonContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/EphemeralInteractionButtonContext.kt index af204da993..0872882914 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/EphemeralInteractionButtonContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/EphemeralInteractionButtonContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.ButtonInteractionCreateEvent /** Class representing the execution context for an ephemeral-only button. **/ public class EphemeralInteractionButtonContext( - override val component: EphemeralInteractionButton, - override val event: ButtonInteractionCreateEvent, - override val interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap + override val component: EphemeralInteractionButton, + override val event: ButtonInteractionCreateEvent, + override val interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : InteractionButtonContext(component, event, cache), EphemeralInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButton.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButton.kt index f5d6eaff65..076a3df8ef 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButton.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButton.kt @@ -12,14 +12,14 @@ import dev.kord.common.entity.DiscordPartialEmoji /** Abstract class representing a button component. **/ public abstract class InteractionButton : Component(), HasPartialEmoji { - /** Button label, for display on Discord. **/ - public var label: String? = null + /** Button label, for display on Discord. **/ + public var label: String? = null - public override var partialEmoji: DiscordPartialEmoji? = null + public override var partialEmoji: DiscordPartialEmoji? = null - override fun validate() { - if (label == null && partialEmoji == null) { - error("Buttons must have either a label or emoji.") - } - } + override fun validate() { + if (label == null && partialEmoji == null) { + error("Buttons must have either a label or emoji.") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonContext.kt index b2a9b6af06..bb9bb873ce 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonContext.kt @@ -14,7 +14,7 @@ import dev.kord.core.event.interaction.ButtonInteractionCreateEvent /** Abstract class representing the execution context for a button component's action. **/ @Suppress("UnnecessaryAbstractClass") // Your face is an unnecessary abstract class public abstract class InteractionButtonContext( - component: Component, - event: ButtonInteractionCreateEvent, - cache: MutableStringKeyedMap + component: Component, + event: ButtonInteractionCreateEvent, + cache: MutableStringKeyedMap, ) : ComponentContext(component, event, cache) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonWithAction.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonWithAction.kt index fb17c02421..d40f16a1ce 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonWithAction.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonWithAction.kt @@ -19,30 +19,30 @@ import io.github.oshai.kotlinlogging.KotlinLogging /** Abstract class representing a button component that has a click action. **/ public abstract class InteractionButtonWithAction(timeoutTask: Task?) : - ComponentWithAction(timeoutTask), HasPartialEmoji { - internal val logger: KLogger = KotlinLogging.logger {} + ComponentWithAction(timeoutTask), HasPartialEmoji { + internal val logger: KLogger = KotlinLogging.logger {} - /** Button label, for display on Discord. **/ - public var label: String? = null + /** Button label, for display on Discord. **/ + public var label: String? = null - /** Whether this button is disabled. **/ - public open var disabled: Boolean = false + /** Whether this button is disabled. **/ + public open var disabled: Boolean = false - public override var partialEmoji: DiscordPartialEmoji? = null + public override var partialEmoji: DiscordPartialEmoji? = null - /** Mark this button as disabled. **/ - public open fun disable() { - disabled = true - } + /** Mark this button as disabled. **/ + public open fun disable() { + disabled = true + } - /** Mark this button as enabled. **/ - public open fun enable() { - disabled = false - } + /** Mark this button as enabled. **/ + public open fun enable() { + disabled = false + } - /** If enabled, adds the initial Sentry breadcrumb to the given context. **/ - public open suspend fun firstSentryBreadcrumb(context: C, button: InteractionButtonWithAction<*, *>) { - if (sentry.enabled) { + /** If enabled, adds the initial Sentry breadcrumb to the given context. **/ + public open suspend fun firstSentryBreadcrumb(context: C, button: InteractionButtonWithAction<*, *>) { + if (sentry.enabled) { context.sentry.context( "component", @@ -52,29 +52,29 @@ public abstract class InteractionButtonWithAction) { - logger.error(t) { "Error during execution of button (${button.id}) action (${context.event})" } + logger.error(t) { "Error during execution of button (${button.id}) action (${context.event})" } - if (sentry.enabled) { - logger.trace { "Submitting error to sentry." } + if (sentry.enabled) { + logger.trace { "Submitting error to sentry." } - val sentryId = context.sentry.captureThrowable(t) { + val sentryId = context.sentry.captureThrowable(t) { channel = context.channel.asChannelOrNull() user = context.user.asUserOrNull() - } + } val errorMessage = if (sentryId != null) { logger.info { "Error submitted to Sentry: $sentryId" } @@ -89,23 +89,23 @@ public abstract class InteractionButtonWithAction) + } else { + respondText( + context, + context.translate("commands.error.user", null), + FailureReason.ExecutionError(t) + ) + } + } + + override fun validate() { + super.validate() + + if (label == null && partialEmoji == null) { + error("Buttons must have either a label or emoji.") + } + } + + /** Override this to implement a way to respond to the user, regardless of whatever happens. **/ + public abstract suspend fun respondText(context: C, message: String, failureType: FailureReason<*>) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonWithID.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonWithID.kt index 5a9f21eb7c..887021e228 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonWithID.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/InteractionButtonWithID.kt @@ -12,15 +12,15 @@ import dev.kord.common.entity.DiscordPartialEmoji /** Abstract class representing a button component with an ID, but without a click action. **/ public abstract class InteractionButtonWithID : ComponentWithID(), HasPartialEmoji { - /** Button label, for display on Discord. **/ - public var label: String? = null - public override var partialEmoji: DiscordPartialEmoji? = null + /** Button label, for display on Discord. **/ + public var label: String? = null + public override var partialEmoji: DiscordPartialEmoji? = null - override fun validate() { - super.validate() + override fun validate() { + super.validate() - if (label == null && partialEmoji == null) { - error("Buttons must have either a label or emoji.") - } - } + if (label == null && partialEmoji == null) { + error("Buttons must have either a label or emoji.") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/LinkInteractionButton.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/LinkInteractionButton.kt index 49ff78d30d..6542dc3068 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/LinkInteractionButton.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/LinkInteractionButton.kt @@ -10,21 +10,21 @@ import dev.kord.rest.builder.component.ActionRowBuilder /** Class representing a linked button component, which opens a URL when clicked. **/ public open class LinkInteractionButton : InteractionButton() { - /** URL to send the user to when clicked. **/ - public open lateinit var url: String + /** URL to send the user to when clicked. **/ + public open lateinit var url: String - override fun validate() { - super.validate() + override fun validate() { + super.validate() - if (!this::url.isInitialized) { - error("Link buttons must have a URL.") - } - } + if (!this::url.isInitialized) { + error("Link buttons must have a URL.") + } + } - override fun apply(builder: ActionRowBuilder) { - builder.linkButton(url) { - emoji = partialEmoji - label = this@LinkInteractionButton.label - } - } + override fun apply(builder: ActionRowBuilder) { + builder.linkButton(url) { + emoji = partialEmoji + label = this@LinkInteractionButton.label + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/PublicInteractionButton.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/PublicInteractionButton.kt index 03df43ba4a..8cc07f7c93 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/PublicInteractionButton.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/PublicInteractionButton.kt @@ -25,123 +25,123 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialPublicButtonResponseBuilder = - (suspend InteractionResponseCreateBuilder.(ButtonInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(ButtonInteractionCreateEvent) -> Unit)? /** Class representing a public-only button component. **/ public open class PublicInteractionButton( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : InteractionButtonWithAction, M>(timeoutTask) { - /** Button style - anything but Link is valid. **/ - public open var style: ButtonStyle = ButtonStyle.Primary - - /** @suppress Initial response builder. **/ - public open var initialResponseBuilder: InitialPublicButtonResponseBuilder = null - - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialPublicButtonResponseBuilder) { - initialResponseBuilder = body - } - - override fun apply(builder: ActionRowBuilder) { - builder.interactionButton(style, id) { - emoji = partialEmoji - label = this@PublicInteractionButton.label - - disabled = this@PublicInteractionButton.disabled - } - } - - override suspend fun call(event: ButtonInteractionCreateEvent): Unit = withLock { - val cache: MutableStringKeyedMap = mutableMapOf() - - super.call(event) - - try { - if (!runChecks(event, cache)) { - return@withLock - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - return@withLock - } - - val modalObj = modal?.invoke() - - val response = if (initialResponseBuilder != null) { - event.interaction.respondPublic { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) - - val locale = event.getLocale() - - event.interaction.modal( - modalObj.translateTitle(locale, bundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), bundle) - } - - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) - - if (!deferredAck) { - it?.deferPublicResponseUnsafe() - } else { - it?.deferPublicMessageUpdate() - } - } ?: return@withLock - } else { - if (!deferredAck) { - event.interaction.deferPublicResponseUnsafe() - } else { - event.interaction.deferPublicMessageUpdate() - } - } - - val context = PublicInteractionButtonContext(this, event, response, cache) - - context.populate() - - firstSentryBreadcrumb(context, this) - - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - - return@withLock - } - - try { - body(context, modalObj) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.RelayedFailure(e)) - } catch (t: Throwable) { - handleError(context, t, this) - } - } - - override fun validate() { - super.validate() - - if (modal != null && initialResponseBuilder != null) { - error("You may not provide a modal builder and an initial response - pick one, not both.") - } - - if (style == ButtonStyle.Link) { - error("The Link button style is reserved for link buttons.") - } - } - - override suspend fun respondText( - context: PublicInteractionButtonContext, - message: String, - failureType: FailureReason<*> - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + /** Button style - anything but Link is valid. **/ + public open var style: ButtonStyle = ButtonStyle.Primary + + /** @suppress Initial response builder. **/ + public open var initialResponseBuilder: InitialPublicButtonResponseBuilder = null + + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialPublicButtonResponseBuilder) { + initialResponseBuilder = body + } + + override fun apply(builder: ActionRowBuilder) { + builder.interactionButton(style, id) { + emoji = partialEmoji + label = this@PublicInteractionButton.label + + disabled = this@PublicInteractionButton.disabled + } + } + + override suspend fun call(event: ButtonInteractionCreateEvent): Unit = withLock { + val cache: MutableStringKeyedMap = mutableMapOf() + + super.call(event) + + try { + if (!runChecks(event, cache)) { + return@withLock + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + return@withLock + } + + val modalObj = modal?.invoke() + + val response = if (initialResponseBuilder != null) { + event.interaction.respondPublic { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) + + val locale = event.getLocale() + + event.interaction.modal( + modalObj.translateTitle(locale, bundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), bundle) + } + + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) + + if (!deferredAck) { + it?.deferPublicResponseUnsafe() + } else { + it?.deferPublicMessageUpdate() + } + } ?: return@withLock + } else { + if (!deferredAck) { + event.interaction.deferPublicResponseUnsafe() + } else { + event.interaction.deferPublicMessageUpdate() + } + } + + val context = PublicInteractionButtonContext(this, event, response, cache) + + context.populate() + + firstSentryBreadcrumb(context, this) + + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + + return@withLock + } + + try { + body(context, modalObj) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.RelayedFailure(e)) + } catch (t: Throwable) { + handleError(context, t, this) + } + } + + override fun validate() { + super.validate() + + if (modal != null && initialResponseBuilder != null) { + error("You may not provide a modal builder and an initial response - pick one, not both.") + } + + if (style == ButtonStyle.Link) { + error("The Link button style is reserved for link buttons.") + } + } + + override suspend fun respondText( + context: PublicInteractionButtonContext, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/PublicInteractionButtonContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/PublicInteractionButtonContext.kt index b6b314011d..3484915120 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/PublicInteractionButtonContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/buttons/PublicInteractionButtonContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.ButtonInteractionCreateEvent /** Class representing the execution context for a public-only button. **/ public class PublicInteractionButtonContext( - component: PublicInteractionButton, - event: ButtonInteractionCreateEvent, - override val interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap + component: PublicInteractionButton, + event: ButtonInteractionCreateEvent, + override val interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : InteractionButtonContext(component, event, cache), PublicInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/Form.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/Form.kt index 9da1849e64..2c512d42a6 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/Form.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/Form.kt @@ -16,14 +16,14 @@ import kotlin.time.Duration */ @Suppress("UnnecessaryAbstractClass") public abstract class Form { - /** How long to wait before we stop waiting for this modal to be submitted. **/ - public open val timeout: Duration = Duration.ZERO + /** How long to wait before we stop waiting for this modal to be submitted. **/ + public open val timeout: Duration = Duration.ZERO // /** Widgets that haven't been sorted into the grid by [pack] yet. **/ // public open val unsortedComponents: MutableList = mutableListOf() - /** A grid containing rows of widgets. **/ - public open val grid: WidgetGrid = WidgetGrid() + /** A grid containing rows of widgets. **/ + public open val grid: WidgetGrid = WidgetGrid() // public abstract fun pack() // public abstract fun update() diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/ModalForm.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/ModalForm.kt index 71d5754574..972659505a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/ModalForm.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/ModalForm.kt @@ -159,7 +159,7 @@ public abstract class ModalForm : Form(), KordExKoinComponent { bundle: String?, interaction: ModalParentInteractionBehavior, callback: suspend (ModalSubmitInteraction?) -> T, - ): T { + ): T { componentRegistry.register(this) interaction.modal(translateTitle(locale, bundle), id) { @@ -178,7 +178,7 @@ public abstract class ModalForm : Form(), KordExKoinComponent { public suspend fun sendAndAwait( context: EventContext, callback: suspend (ModalSubmitInteraction?) -> T, - ): T { + ): T { val interaction = context.event.interaction as? ModalParentInteractionBehavior ?: error("Interaction ${context.event.interaction} does not support responding with a modal.") @@ -194,7 +194,7 @@ public abstract class ModalForm : Form(), KordExKoinComponent { public suspend fun sendAndAwait( context: ApplicationCommandContext, callback: suspend (ModalSubmitInteraction?) -> T, - ): T { + ): T { val interaction = context.genericEvent.interaction as? ModalParentInteractionBehavior ?: error("Interaction ${context.genericEvent.interaction} does not support responding with a modal.") @@ -210,7 +210,7 @@ public abstract class ModalForm : Form(), KordExKoinComponent { public suspend fun sendAndAwait( context: ComponentContext<*>, callback: suspend (ModalSubmitInteraction?) -> T, - ): T { + ): T { val interaction = context.event.interaction as? ModalParentInteractionBehavior ?: error("Interaction ${context.event.interaction} does not support responding with a modal.") diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/_Grid.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/_Grid.kt index 17896960d9..c4fa60c1a7 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/_Grid.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/_Grid.kt @@ -23,125 +23,125 @@ public const val GRID_CAPACITY: Int = GRID_WIDTH * GRID_HEIGHT // region: Coordinate Functions public infix fun Int.x(other: Int): CoordinatePair = - CoordinatePair(this, other) + CoordinatePair(this, other) @Suppress("UnnecessaryParentheses") public operator fun CoordinatePair.plus(other: CoordinatePair): Pair = - (first + other.first) x (second + other.second) + (first + other.first) x (second + other.second) @Suppress("UnnecessaryParentheses") public operator fun CoordinatePair.minus(other: CoordinatePair): Pair = - (first - other.first) x (second - other.second) + (first - other.first) x (second - other.second) public fun CoordinatePair.permutationsUpto(end: CoordinatePair): MutableList { - val values = mutableListOf() + val values = mutableListOf() - for (row in first..end.first) { - for (column in second..end.second) { - values.add(row x column) - } - } + for (row in first..end.first) { + for (column in second..end.second) { + values.add(row x column) + } + } - return values + return values } public fun CoordinatePair.permutationsUptoExclusive(end: CoordinatePair): MutableList { - val values = mutableListOf() + val values = mutableListOf() - for (row in first until end.first) { - for (column in second until end.second) { - values.add(row x column) - } - } + for (row in first until end.first) { + for (column in second until end.second) { + values.add(row x column) + } + } - return values + return values } public fun CoordinatePair.forEachUpto(end: CoordinatePair, body: (CoordinatePair) -> Unit) { - for (row in first..end.first) { - for (column in second..end.second) { - body(row x column) - } - } + for (row in first..end.first) { + for (column in second..end.second) { + body(row x column) + } + } } public fun CoordinatePair.forEachUptoExclusive(end: CoordinatePair, body: (CoordinatePair) -> Unit) { - for (row in first until end.first) { - for (column in second until end.second) { - body(row x column) - } - } + for (row in first until end.first) { + for (column in second until end.second) { + body(row x column) + } + } } public inline fun CoordinatePair.mapUpto(end: CoordinatePair, body: (CoordinatePair) -> T): List { - val values = mutableListOf() + val values = mutableListOf() - for (row in first..end.first) { - for (column in second..end.second) { - values.add(body(row x column)) - } - } + for (row in first..end.first) { + for (column in second..end.second) { + values.add(body(row x column)) + } + } - return values + return values } public inline fun CoordinatePair.mapUptoExclusive(end: CoordinatePair, body: (CoordinatePair) -> T): List { - val values = mutableListOf() + val values = mutableListOf() - for (row in first until end.first) { - for (column in second until end.second) { - values.add(body(row x column)) - } - } + for (row in first until end.first) { + for (column in second until end.second) { + values.add(body(row x column)) + } + } - return values + return values } public inline fun CoordinatePair.mapNotNullUpto(end: CoordinatePair, body: (CoordinatePair) -> T?): List { - val values = mutableListOf() + val values = mutableListOf() - for (row in first..end.first) { - for (column in second..end.second) { - val result = body(row x column) - ?: continue + for (row in first..end.first) { + for (column in second..end.second) { + val result = body(row x column) + ?: continue - values.add(result) - } - } + values.add(result) + } + } - return values + return values } public inline fun CoordinatePair.mapNotNullUptoExclusive( - end: CoordinatePair, - body: (CoordinatePair) -> T?, + end: CoordinatePair, + body: (CoordinatePair) -> T?, ): List { - val values = mutableListOf() + val values = mutableListOf() - for (row in first until end.first) { - for (column in second until end.second) { - val result = body(row x column) - ?: continue + for (row in first until end.first) { + for (column in second until end.second) { + val result = body(row x column) + ?: continue - values.add(result) - } - } + values.add(result) + } + } - return values + return values } public fun CoordinatePair.isValid(): Boolean = - first in 0 until GRID_HEIGHT && - second in 0 until GRID_WIDTH + first in 0 until GRID_HEIGHT && + second in 0 until GRID_WIDTH public fun CoordinatePair.throwIfInvalid(name: String = "Coordinate") { - if (!isValid()) { - error("$name row and column must be within 0 <= x < $GRID_HEIGHT; got $this") - } + if (!isValid()) { + error("$name row and column must be within 0 <= x < $GRID_HEIGHT; got $this") + } } public fun CoordinatePair.toString(): String = - "($first x $second)" + "($first x $second)" // endregion @@ -149,121 +149,121 @@ public fun CoordinatePair.toString(): String = @Suppress("MagicNumber") public fun WidgetGrid.toString(): String = buildString { - val grid = this@toString - - appendLine("WidgetGrid@${hashCode()} -> ") - - append( - grid.joinToString(",\n") { row -> - val rowString = row.joinToString(" | ") { widget -> - if (widget == null) { - "null" - } else { - widget::class.simpleName ?: "null" - }.padEnd(20) - } - - " | $rowString |" - } - ) + val grid = this@toString + + appendLine("WidgetGrid@${hashCode()} -> ") + + append( + grid.joinToString(",\n") { row -> + val rowString = row.joinToString(" | ") { widget -> + if (widget == null) { + "null" + } else { + widget::class.simpleName ?: "null" + }.padEnd(20) + } + + " | $rowString |" + } + ) } @Suppress("USELESS_CAST", "SpreadOperator") // You're just wrong here, IJ public fun WidgetGrid(): WidgetGrid = arrayOf( - *GRID_HEIGHT.map { - GRID_WIDTH.map { null as Widget<*>? }.toMutableList() - }.toTypedArray() + *GRID_HEIGHT.map { + GRID_WIDTH.map { null as Widget<*>? }.toMutableList() + }.toTypedArray() ) public fun WidgetGrid.get(coordinate: CoordinatePair): Widget<*>? { - coordinate.throwIfInvalid() + coordinate.throwIfInvalid() - return this[coordinate.first].getOrNull(coordinate.second) + return this[coordinate.first].getOrNull(coordinate.second) } @Suppress("UnnecessaryParentheses") public fun WidgetGrid.setAtCoordinateOrFirstRow(coordinate: CoordinatePair?, widget: Widget<*>) { - if (coordinate == null) { - val row = indexOfFirst { (GRID_WIDTH - it.count { e -> e != null }) >= widget.width } + if (coordinate == null) { + val row = indexOfFirst { (GRID_WIDTH - it.count { e -> e != null }) >= widget.width } - if (row == -1) { - error("No rows are available to fit this widget into.") - } + if (row == -1) { + error("No rows are available to fit this widget into.") + } - val column = get(row).indexOf(null) + val column = get(row).indexOf(null) - set(row x column, widget) - } else { - set(coordinate, widget) - } + set(row x column, widget) + } else { + set(coordinate, widget) + } } public fun WidgetGrid.set(coordinate: CoordinatePair, widget: Widget<*>) { - coordinate.throwIfInvalid("Start coordinate") + coordinate.throwIfInvalid("Start coordinate") - val end = coordinate + (widget.height - 1 x widget.width - 1) + val end = coordinate + (widget.height - 1 x widget.width - 1) - end.throwIfInvalid("End coordinate") + end.throwIfInvalid("End coordinate") - val permutations = coordinate.permutationsUpto(end) - val existing = permutations.mapNotNull { get(it) }.toSet() + val permutations = coordinate.permutationsUpto(end) + val existing = permutations.mapNotNull { get(it) }.toSet() - if (existing.isNotEmpty()) { - error("Can't add widget from $coordinate to $end as it would overlap ${existing.size} other widgets") - } + if (existing.isNotEmpty()) { + error("Can't add widget from $coordinate to $end as it would overlap ${existing.size} other widgets") + } - permutations.forEach { - this[it.first][it.second] = widget - } + permutations.forEach { + this[it.first][it.second] = widget + } } public fun WidgetGrid.removeAt(coordinate: CoordinatePair): Boolean = - if (get(coordinate) != null) { - this[coordinate.first].removeAt(coordinate.second) + if (get(coordinate) != null) { + this[coordinate.first].removeAt(coordinate.second) - true - } else { - false - } + true + } else { + false + } public fun WidgetGrid.remove(widget: Widget<*>): Boolean { - val coordinates = coordinatesFor(widget) + val coordinates = coordinatesFor(widget) - if (coordinates.isEmpty()) { - return false - } + if (coordinates.isEmpty()) { + return false + } - coordinates.map(::removeAt) + coordinates.map(::removeAt) - return true + return true } public fun WidgetGrid.coordinatesFor(widget: Widget<*>): List { - val values = mutableListOf() + val values = mutableListOf() - forEachIndexed { rowIndex, row -> - row.forEachIndexed { columnIndex, storedWidget -> - if (widget === storedWidget) { - values.add(rowIndex to columnIndex) - } - } - } + forEachIndexed { rowIndex, row -> + row.forEachIndexed { columnIndex, storedWidget -> + if (widget === storedWidget) { + values.add(rowIndex to columnIndex) + } + } + } - return values + return values } public fun WidgetGrid.getWidgetSet(): Set> { - val values = mutableSetOf>() + val values = mutableSetOf>() - forEach { row -> - row.forEach { widget -> - if (widget != null) { - values.add(widget) - } - } - } + forEach { row -> + row.forEach { widget -> + if (widget != null) { + values.add(widget) + } + } + } - return values + return values } // endregion diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/LineTextWidget.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/LineTextWidget.kt index 5059652200..c77b3ef979 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/LineTextWidget.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/LineTextWidget.kt @@ -10,5 +10,5 @@ import dev.kord.common.entity.TextInputStyle /** A text widget that supports a single line of text. **/ public class LineTextWidget : TextInputWidget() { - override val style: TextInputStyle = TextInputStyle.Short + override val style: TextInputStyle = TextInputStyle.Short } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/ParagraphTextWidget.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/ParagraphTextWidget.kt index 683b50654e..105987465c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/ParagraphTextWidget.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/ParagraphTextWidget.kt @@ -10,5 +10,5 @@ import dev.kord.common.entity.TextInputStyle /** A text widget that supports multiple lines of text. **/ public class ParagraphTextWidget : TextInputWidget() { - override val style: TextInputStyle = TextInputStyle.Paragraph + override val style: TextInputStyle = TextInputStyle.Paragraph } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/TextInputWidget.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/TextInputWidget.kt index eb5875f543..d403c28c7d 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/TextInputWidget.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/TextInputWidget.kt @@ -27,93 +27,93 @@ public const val PLACEHOLDER_LENGTH: Int = 100 /** An abstract type representing a widget that accepts text from the user. */ public abstract class TextInputWidget> : Widget(), KordExKoinComponent { - @Suppress("MagicNumber") - override var width: Int = 5 - override var height: Int = 1 - override var value: String? = null - - /** The [TextInputStyle], to be provided by a subtype. **/ - public abstract val style: TextInputStyle - - /** The widget's label, to be shown on Discord. **/ - public lateinit var label: String - - /** The widget's unique ID on Discord, defaulting to a UUID. **/ - public var id: String = UUID.randomUUID().toString() - - /** The initial value to provide for this widget, if any. **/ - public var initialValue: String? = null - - /** The maximum number of characters that may be provided. **/ - public var maxLength: Int = MAX_LENGTH - - /** The minimum number of characters that may be provided. **/ - public var minLength: Int = MIN_LENGTH - - /** Placeholder text, to be shown to the user on Discord. **/ - public var placeholder: String? = null - - /** Whether this widget must be filled out for the form to be valid. **/ - public var required: Boolean = true - - /** @suppress Translations provider reference, used internally **/ - public val translations: TranslationsProvider by inject() - - public override fun validate() { - if (this::label.isInitialized.not() || label.isEmpty()) { - error("Text input widgets must be given a label, but no label was provided.") - } - - if (label.length > LABEL_LENGTH) { - error("Labels must be shorter than $LABEL_LENGTH characters, but ${label.length} characters were provided.") - } - - @Suppress("UnnecessaryParentheses") - if (maxLength !in (MIN_LENGTH + 1)..MAX_LENGTH) { - error( - "Invalid value for maxLength provided: $maxLength - expected ${MIN_LENGTH + 1} - $MAX_LENGTH" - ) - } - - if (minLength !in MIN_LENGTH until MAX_LENGTH) { - error( - "Invalid value for minLength provided: $minLength - expected $MIN_LENGTH - ${MAX_LENGTH - 1}" - ) - } - - if (initialValue != null && (initialValue!!.length > MAX_LENGTH || initialValue!!.isEmpty())) { - error( - "Invalid value for $initialValue provided: (${initialValue!!.length} chars) - expected " + - "${MIN_LENGTH + 1} - $MAX_LENGTH" - ) - } - - if (placeholder != null && (placeholder!!.length > PLACEHOLDER_LENGTH || placeholder!!.isEmpty())) { - error( - "Invalid value for $placeholder provided: (${placeholder!!.length} chars) - expected " + - "${MIN_LENGTH + 1} - $PLACEHOLDER_LENGTH" - ) - } - } - - override suspend fun apply(builder: ActionRowBuilder, locale: Locale, bundle: String?) { - builder.textInput(style, id, translations.translate(label, locale, bundle)) { - this.allowedLength = this@TextInputWidget.minLength..this@TextInputWidget.maxLength - this.required = this@TextInputWidget.required - - this.placeholder = this@TextInputWidget.placeholder?.let { - translations.translate(it, locale, bundle) - } - - this.value = this@TextInputWidget.initialValue?.let { - translations.translate(it, locale, bundle) - } - } - } - - /** @suppress Internal API method. **/ - @JvmName("setValue1") - public fun setValue(value: String) { - this.value = value - } + @Suppress("MagicNumber") + override var width: Int = 5 + override var height: Int = 1 + override var value: String? = null + + /** The [TextInputStyle], to be provided by a subtype. **/ + public abstract val style: TextInputStyle + + /** The widget's label, to be shown on Discord. **/ + public lateinit var label: String + + /** The widget's unique ID on Discord, defaulting to a UUID. **/ + public var id: String = UUID.randomUUID().toString() + + /** The initial value to provide for this widget, if any. **/ + public var initialValue: String? = null + + /** The maximum number of characters that may be provided. **/ + public var maxLength: Int = MAX_LENGTH + + /** The minimum number of characters that may be provided. **/ + public var minLength: Int = MIN_LENGTH + + /** Placeholder text, to be shown to the user on Discord. **/ + public var placeholder: String? = null + + /** Whether this widget must be filled out for the form to be valid. **/ + public var required: Boolean = true + + /** @suppress Translations provider reference, used internally **/ + public val translations: TranslationsProvider by inject() + + public override fun validate() { + if (this::label.isInitialized.not() || label.isEmpty()) { + error("Text input widgets must be given a label, but no label was provided.") + } + + if (label.length > LABEL_LENGTH) { + error("Labels must be shorter than $LABEL_LENGTH characters, but ${label.length} characters were provided.") + } + + @Suppress("UnnecessaryParentheses") + if (maxLength !in (MIN_LENGTH + 1)..MAX_LENGTH) { + error( + "Invalid value for maxLength provided: $maxLength - expected ${MIN_LENGTH + 1} - $MAX_LENGTH" + ) + } + + if (minLength !in MIN_LENGTH until MAX_LENGTH) { + error( + "Invalid value for minLength provided: $minLength - expected $MIN_LENGTH - ${MAX_LENGTH - 1}" + ) + } + + if (initialValue != null && (initialValue!!.length > MAX_LENGTH || initialValue!!.isEmpty())) { + error( + "Invalid value for $initialValue provided: (${initialValue!!.length} chars) - expected " + + "${MIN_LENGTH + 1} - $MAX_LENGTH" + ) + } + + if (placeholder != null && (placeholder!!.length > PLACEHOLDER_LENGTH || placeholder!!.isEmpty())) { + error( + "Invalid value for $placeholder provided: (${placeholder!!.length} chars) - expected " + + "${MIN_LENGTH + 1} - $PLACEHOLDER_LENGTH" + ) + } + } + + override suspend fun apply(builder: ActionRowBuilder, locale: Locale, bundle: String?) { + builder.textInput(style, id, translations.translate(label, locale, bundle)) { + this.allowedLength = this@TextInputWidget.minLength..this@TextInputWidget.maxLength + this.required = this@TextInputWidget.required + + this.placeholder = this@TextInputWidget.placeholder?.let { + translations.translate(it, locale, bundle) + } + + this.value = this@TextInputWidget.initialValue?.let { + translations.translate(it, locale, bundle) + } + } + } + + /** @suppress Internal API method. **/ + @JvmName("setValue1") + public fun setValue(value: String) { + this.value = value + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/Widget.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/Widget.kt index c99122095e..a4aadd56a4 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/Widget.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/forms/widgets/Widget.kt @@ -11,24 +11,24 @@ import java.util.* /** Abstract type representing a grid-based widget. **/ public abstract class Widget { - /** How wide this widget is, in grid cells. **/ - public abstract var width: Int - protected set + /** How wide this widget is, in grid cells. **/ + public abstract var width: Int + protected set - /** How tall this widget is, in grid cells. **/ - public abstract var height: Int - protected set + /** How tall this widget is, in grid cells. **/ + public abstract var height: Int + protected set - /** The final value stored in this widget, as provided by the user. **/ - public abstract var value: T - protected set + /** The final value stored in this widget, as provided by the user. **/ + public abstract var value: T + protected set - override fun toString(): String = - "${this::class.simpleName}@${hashCode()} ($width x $height)" + override fun toString(): String = + "${this::class.simpleName}@${hashCode()} ($width x $height)" - /** Function called to apply this widget to a Discord action row. **/ - public abstract suspend fun apply(builder: ActionRowBuilder, locale: Locale, bundle: String?) + /** Function called to apply this widget to a Discord action row. **/ + public abstract suspend fun apply(builder: ActionRowBuilder, locale: Locale, bundle: String?) - /** Function called to ensure that this widget was set up correctly. **/ - public abstract fun validate() + /** Function called to ensure that this widget was set up correctly. **/ + public abstract fun validate() } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/EphemeralSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/EphemeralSelectMenu.kt index 121bdac9e2..d907639921 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/EphemeralSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/EphemeralSelectMenu.kt @@ -22,112 +22,112 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing an ephemeral-only select (dropdown) menu. **/ public abstract class EphemeralSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, - /** Builder for the initial response, omit to ack instead. **/ - public open var initialResponseBuilder: InitialEphemeralSelectMenuResponseBuilder = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, + /** Builder for the initial response, omit to ack instead. **/ + public open var initialResponseBuilder: InitialEphemeralSelectMenuResponseBuilder = null, ) : SelectMenu(timeoutTask) where C : SelectMenuContext, C : EphemeralInteractionContext { - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialEphemeralSelectMenuResponseBuilder) { - initialResponseBuilder = body - } - - override fun validate() { - super.validate() - - if (modal != null && initialResponseBuilder != null) { - error("You may not provide a modal builder and an initial response - pick one, not both.") - } - } - - /** Function to create the context of the select menu. **/ - public abstract fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): C - - @OptIn(KordUnsafe::class) - @Suppress("TooGenericExceptionCaught") - override suspend fun call(event: SelectMenuInteractionCreateEvent): Unit = withLock { - val cache: MutableStringKeyedMap = mutableMapOf() - - super.call(event) - - try { - if (!runChecks(event, cache)) { - return@withLock - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - return@withLock - } - - val modalObj = modal?.invoke() - - val response = if (initialResponseBuilder != null) { - event.interaction.respondEphemeral { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) - - val locale = event.getLocale() - - event.interaction.modal( - modalObj.translateTitle(locale, bundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), bundle) - } - - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) - - if (!deferredAck) { - it?.deferEphemeralResponseUnsafe() - } else { - it?.deferEphemeralMessageUpdate() - } - } ?: return@withLock - } else { - if (!deferredAck) { - event.interaction.deferEphemeralResponseUnsafe() - } else { - event.interaction.deferEphemeralMessageUpdate() - } - } - - val context = createContext(event, response, cache) - - context.populate() - - firstSentryBreadcrumb(context, this) - - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - - return@withLock - } - - try { - body(context, modalObj) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.RelayedFailure(e)) - } catch (t: Throwable) { - handleError(context, t, this) - } - } - - override suspend fun respondText( - context: C, - message: String, - failureType: FailureReason<*>, - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialEphemeralSelectMenuResponseBuilder) { + initialResponseBuilder = body + } + + override fun validate() { + super.validate() + + if (modal != null && initialResponseBuilder != null) { + error("You may not provide a modal builder and an initial response - pick one, not both.") + } + } + + /** Function to create the context of the select menu. **/ + public abstract fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): C + + @OptIn(KordUnsafe::class) + @Suppress("TooGenericExceptionCaught") + override suspend fun call(event: SelectMenuInteractionCreateEvent): Unit = withLock { + val cache: MutableStringKeyedMap = mutableMapOf() + + super.call(event) + + try { + if (!runChecks(event, cache)) { + return@withLock + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + return@withLock + } + + val modalObj = modal?.invoke() + + val response = if (initialResponseBuilder != null) { + event.interaction.respondEphemeral { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) + + val locale = event.getLocale() + + event.interaction.modal( + modalObj.translateTitle(locale, bundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), bundle) + } + + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) + + if (!deferredAck) { + it?.deferEphemeralResponseUnsafe() + } else { + it?.deferEphemeralMessageUpdate() + } + } ?: return@withLock + } else { + if (!deferredAck) { + event.interaction.deferEphemeralResponseUnsafe() + } else { + event.interaction.deferEphemeralMessageUpdate() + } + } + + val context = createContext(event, response, cache) + + context.populate() + + firstSentryBreadcrumb(context, this) + + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + + return@withLock + } + + try { + body(context, modalObj) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.RelayedFailure(e)) + } catch (t: Throwable) { + handleError(context, t, this) + } + } + + override suspend fun respondText( + context: C, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/PublicSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/PublicSelectMenu.kt index d81513121b..1674092142 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/PublicSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/PublicSelectMenu.kt @@ -23,112 +23,112 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing a public-only select (dropdown) menu. **/ public abstract class PublicSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, - /** The initial response builder, omit to ack instead. **/ - public open var initialResponseBuilder: InitialEphemeralSelectMenuResponseBuilder = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, + /** The initial response builder, omit to ack instead. **/ + public open var initialResponseBuilder: InitialEphemeralSelectMenuResponseBuilder = null, ) : SelectMenu(timeoutTask) where C : SelectMenuContext, C : PublicInteractionContext { - /** Call this to open with a response, omit it to ack instead. **/ - public fun initialResponse(body: InitialEphemeralSelectMenuResponseBuilder) { - initialResponseBuilder = body - } - - override fun validate() { - super.validate() - - if (modal != null && initialResponseBuilder != null) { - error("You may not provide a modal builder and an initial response - pick one, not both.") - } - } - - /** Function to create the context for the select menu. **/ - public abstract fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): C - - @OptIn(KordUnsafe::class) - @Suppress("TooGenericExceptionCaught") - override suspend fun call(event: SelectMenuInteractionCreateEvent): Unit = withLock { - val cache: MutableStringKeyedMap = mutableMapOf() - - super.call(event) - - try { - if (!runChecks(event, cache)) { - return@withLock - } - } catch (e: DiscordRelayedException) { - event.interaction.respondEphemeral { - settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) - } - - return@withLock - } - - val modalObj = modal?.invoke() - - val response = if (initialResponseBuilder != null) { - event.interaction.respondPublic { initialResponseBuilder!!(event) } - } else if (modalObj != null) { - componentRegistry.register(modalObj) - - val locale = event.getLocale() - - event.interaction.modal( - modalObj.translateTitle(locale, bundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, event.getLocale(), bundle) - } - - modalObj.awaitCompletion { - componentRegistry.unregisterModal(modalObj) - - if (!deferredAck) { - it?.deferPublicResponseUnsafe() - } else { - it?.deferPublicMessageUpdate() - } - } ?: return@withLock - } else { - if (!deferredAck) { - event.interaction.deferPublicResponseUnsafe() - } else { - event.interaction.deferPublicMessageUpdate() - } - } - - val context = createContext(event, response, cache) - - context.populate() - - firstSentryBreadcrumb(context, this) - - try { - checkBotPerms(context) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) - - return@withLock - } - - try { - body(context, modalObj) - } catch (e: DiscordRelayedException) { - respondText(context, e.reason, FailureReason.RelayedFailure(e)) - } catch (t: Throwable) { - handleError(context, t, this) - } - } - - override suspend fun respondText( - context: C, - message: String, - failureType: FailureReason<*>, - ) { - context.respond { settings.failureResponseBuilder(this, message, failureType) } - } + /** Call this to open with a response, omit it to ack instead. **/ + public fun initialResponse(body: InitialEphemeralSelectMenuResponseBuilder) { + initialResponseBuilder = body + } + + override fun validate() { + super.validate() + + if (modal != null && initialResponseBuilder != null) { + error("You may not provide a modal builder and an initial response - pick one, not both.") + } + } + + /** Function to create the context for the select menu. **/ + public abstract fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): C + + @OptIn(KordUnsafe::class) + @Suppress("TooGenericExceptionCaught") + override suspend fun call(event: SelectMenuInteractionCreateEvent): Unit = withLock { + val cache: MutableStringKeyedMap = mutableMapOf() + + super.call(event) + + try { + if (!runChecks(event, cache)) { + return@withLock + } + } catch (e: DiscordRelayedException) { + event.interaction.respondEphemeral { + settings.failureResponseBuilder(this, e.reason, FailureReason.ProvidedCheckFailure(e)) + } + + return@withLock + } + + val modalObj = modal?.invoke() + + val response = if (initialResponseBuilder != null) { + event.interaction.respondPublic { initialResponseBuilder!!(event) } + } else if (modalObj != null) { + componentRegistry.register(modalObj) + + val locale = event.getLocale() + + event.interaction.modal( + modalObj.translateTitle(locale, bundle), + modalObj.id + ) { + modalObj.applyToBuilder(this, event.getLocale(), bundle) + } + + modalObj.awaitCompletion { + componentRegistry.unregisterModal(modalObj) + + if (!deferredAck) { + it?.deferPublicResponseUnsafe() + } else { + it?.deferPublicMessageUpdate() + } + } ?: return@withLock + } else { + if (!deferredAck) { + event.interaction.deferPublicResponseUnsafe() + } else { + event.interaction.deferPublicMessageUpdate() + } + } + + val context = createContext(event, response, cache) + + context.populate() + + firstSentryBreadcrumb(context, this) + + try { + checkBotPerms(context) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.OwnPermissionsCheckFailure(e)) + + return@withLock + } + + try { + body(context, modalObj) + } catch (e: DiscordRelayedException) { + respondText(context, e.reason, FailureReason.RelayedFailure(e)) + } catch (t: Throwable) { + handleError(context, t, this) + } + } + + override suspend fun respondText( + context: C, + message: String, + failureType: FailureReason<*>, + ) { + context.respond { settings.failureResponseBuilder(this, message, failureType) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/SelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/SelectMenuContext.kt index 1cca94e7d3..3a92ec8670 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/SelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/SelectMenuContext.kt @@ -13,7 +13,7 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Abstract class representing the execution context of a select (dropdown) menu component. **/ @Suppress("UnnecessaryAbstractClass") public abstract class SelectMenuContext( - component: SelectMenu<*, *>, - event: SelectMenuInteractionCreateEvent, - cache: MutableStringKeyedMap, + component: SelectMenu<*, *>, + event: SelectMenuInteractionCreateEvent, + cache: MutableStringKeyedMap, ) : ComponentContext(component, event, cache) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/ChannelSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/ChannelSelectMenu.kt index 594558bec1..898f73b4a5 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/ChannelSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/ChannelSelectMenu.kt @@ -13,27 +13,27 @@ import dev.kord.rest.builder.component.ActionRowBuilder /** Interface for channel select menus. **/ public interface ChannelSelectMenu { - /** The types allowed in the select menu. **/ - public var channelTypes: MutableList + /** The types allowed in the select menu. **/ + public var channelTypes: MutableList - /** Add an allowed channel type to the selector. **/ - public fun channelType(vararg type: ChannelType) { - channelTypes.addAll(type) - } + /** Add an allowed channel type to the selector. **/ + public fun channelType(vararg type: ChannelType) { + channelTypes.addAll(type) + } - /** Apply the channel select menu to an action row builder. **/ - public fun applyChannelSelectMenu(selectMenu: SelectMenu<*, *>, builder: ActionRowBuilder) { - if (selectMenu.maximumChoices == null) selectMenu.maximumChoices = OPTIONS_MAX + /** Apply the channel select menu to an action row builder. **/ + public fun applyChannelSelectMenu(selectMenu: SelectMenu<*, *>, builder: ActionRowBuilder) { + if (selectMenu.maximumChoices == null) selectMenu.maximumChoices = OPTIONS_MAX - builder.channelSelect(selectMenu.id) { - this.channelTypes = if (this@ChannelSelectMenu.channelTypes.isEmpty()) { - null - } else { - this@ChannelSelectMenu.channelTypes - } - this.allowedValues = selectMenu.minimumChoices..selectMenu.maximumChoices!! - this.placeholder = selectMenu.placeholder - this.disabled = selectMenu.disabled - } - } + builder.channelSelect(selectMenu.id) { + this.channelTypes = if (this@ChannelSelectMenu.channelTypes.isEmpty()) { + null + } else { + this@ChannelSelectMenu.channelTypes + } + this.allowedValues = selectMenu.minimumChoices..selectMenu.maximumChoices!! + this.placeholder = selectMenu.placeholder + this.disabled = selectMenu.disabled + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/ChannelSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/ChannelSelectMenuContext.kt index acadd11d84..686e42ddfd 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/ChannelSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/ChannelSelectMenuContext.kt @@ -17,13 +17,13 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Abstract class representing the execution context of a channel select (dropdown) menu component. **/ public abstract class ChannelSelectMenuContext( - component: SelectMenu<*, *>, - event: SelectMenuInteractionCreateEvent, - cache: MutableStringKeyedMap, + component: SelectMenu<*, *>, + event: SelectMenuInteractionCreateEvent, + cache: MutableStringKeyedMap, ) : SelectMenuContext(component, event, cache) { - /** Menu options that were selected by the user before de-focusing the menu. **/ - @OptIn(KordUnsafe::class, KordExperimental::class) - public val selected: List by lazy { - event.interaction.values.map { event.kord.unsafe.channel(Snowflake(it)) } - } + /** Menu options that were selected by the user before de-focusing the menu. **/ + @OptIn(KordUnsafe::class, KordExperimental::class) + public val selected: List by lazy { + event.interaction.values.map { event.kord.unsafe.channel(Snowflake(it)) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/EphemeralChannelSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/EphemeralChannelSelectMenu.kt index 8209df30fc..69b212603a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/EphemeralChannelSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/EphemeralChannelSelectMenu.kt @@ -17,22 +17,22 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialEphemeralSelectMenuResponseBuilder = - (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? /** Class representing an ephemeral-only channel select (dropdown) menu. **/ public open class EphemeralChannelSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : EphemeralSelectMenu, M>(timeoutTask), ChannelSelectMenu { - override var channelTypes: MutableList = mutableListOf() + override var channelTypes: MutableList = mutableListOf() - override fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): EphemeralChannelSelectMenuContext = EphemeralChannelSelectMenuContext( - this, event, interactionResponse, cache - ) + override fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): EphemeralChannelSelectMenuContext = EphemeralChannelSelectMenuContext( + this, event, interactionResponse, cache + ) - public override fun apply(builder: ActionRowBuilder): Unit = applyChannelSelectMenu(this, builder) + public override fun apply(builder: ActionRowBuilder): Unit = applyChannelSelectMenu(this, builder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/EphemeralChannelSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/EphemeralChannelSelectMenuContext.kt index 3363a560cb..0df20bde2e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/EphemeralChannelSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/EphemeralChannelSelectMenuContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing the execution context for an ephemeral-only channel select (dropdown) menu. **/ public class EphemeralChannelSelectMenuContext( - override val component: EphemeralChannelSelectMenu, - override val event: SelectMenuInteractionCreateEvent, - override val interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val component: EphemeralChannelSelectMenu, + override val event: SelectMenuInteractionCreateEvent, + override val interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : ChannelSelectMenuContext(component, event, cache), EphemeralInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/PublicChannelSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/PublicChannelSelectMenu.kt index ca9b10bd27..28558eea5e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/PublicChannelSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/PublicChannelSelectMenu.kt @@ -17,22 +17,22 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialPublicSelectMenuResponseBuilder = - (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? /** Class representing a public-only channel select (dropdown) menu. **/ public open class PublicChannelSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : PublicSelectMenu, M>(timeoutTask), ChannelSelectMenu { - override var channelTypes: MutableList = mutableListOf() + override var channelTypes: MutableList = mutableListOf() - override fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): PublicChannelSelectMenuContext = PublicChannelSelectMenuContext( - this, event, interactionResponse, cache - ) + override fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): PublicChannelSelectMenuContext = PublicChannelSelectMenuContext( + this, event, interactionResponse, cache + ) - override fun apply(builder: ActionRowBuilder): Unit = applyChannelSelectMenu(this, builder) + override fun apply(builder: ActionRowBuilder): Unit = applyChannelSelectMenu(this, builder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/PublicChannelSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/PublicChannelSelectMenuContext.kt index 8405d7291b..77eb7d251c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/PublicChannelSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/channel/PublicChannelSelectMenuContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing the execution context for a public-only channel select (dropdown) menu. **/ public class PublicChannelSelectMenuContext( - override val component: PublicChannelSelectMenu, - override val event: SelectMenuInteractionCreateEvent, - override val interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val component: PublicChannelSelectMenu, + override val event: SelectMenuInteractionCreateEvent, + override val interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : ChannelSelectMenuContext(component, event, cache), PublicInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/EphemeralRoleSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/EphemeralRoleSelectMenu.kt index e8d256db5d..a8550e2dc3 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/EphemeralRoleSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/EphemeralRoleSelectMenu.kt @@ -16,20 +16,20 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialEphemeralSelectMenuResponseBuilder = - (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? /** Class representing an ephemeral-only role select (dropdown) menu. **/ public open class EphemeralRoleSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : EphemeralSelectMenu, M>(timeoutTask), RoleSelectMenu { - override fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): EphemeralRoleSelectMenuContext = EphemeralRoleSelectMenuContext( - this, event, interactionResponse, cache - ) + override fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): EphemeralRoleSelectMenuContext = EphemeralRoleSelectMenuContext( + this, event, interactionResponse, cache + ) - override fun apply(builder: ActionRowBuilder): Unit = applyRoleSelectMenu(this, builder) + override fun apply(builder: ActionRowBuilder): Unit = applyRoleSelectMenu(this, builder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/EphemeralRoleSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/EphemeralRoleSelectMenuContext.kt index 7feb6482d7..b46ad55b8e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/EphemeralRoleSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/EphemeralRoleSelectMenuContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing the execution context for an ephemeral-only role select (dropdown) menu. **/ public class EphemeralRoleSelectMenuContext( - override val component: EphemeralRoleSelectMenu, - override val event: SelectMenuInteractionCreateEvent, - override val interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val component: EphemeralRoleSelectMenu, + override val event: SelectMenuInteractionCreateEvent, + override val interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : RoleSelectMenuContext(component, event, cache), EphemeralInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/PublicRoleSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/PublicRoleSelectMenu.kt index f02658c8ec..da01ac674f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/PublicRoleSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/PublicRoleSelectMenu.kt @@ -16,20 +16,20 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialPublicSelectMenuResponseBuilder = - (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? /** Class representing a public-only role select (dropdown) menu. **/ public open class PublicRoleSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : PublicSelectMenu, M>(timeoutTask), RoleSelectMenu { - override fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): PublicRoleSelectMenuContext = PublicRoleSelectMenuContext( - this, event, interactionResponse, cache - ) + override fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): PublicRoleSelectMenuContext = PublicRoleSelectMenuContext( + this, event, interactionResponse, cache + ) - override fun apply(builder: ActionRowBuilder): Unit = applyRoleSelectMenu(this, builder) + override fun apply(builder: ActionRowBuilder): Unit = applyRoleSelectMenu(this, builder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/PublicRoleSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/PublicRoleSelectMenuContext.kt index aed04315c8..cf2b768993 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/PublicRoleSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/PublicRoleSelectMenuContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing the execution context for a public-only role select (dropdown) menu. **/ public class PublicRoleSelectMenuContext( - override val component: PublicRoleSelectMenu, - override val event: SelectMenuInteractionCreateEvent, - override val interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val component: PublicRoleSelectMenu, + override val event: SelectMenuInteractionCreateEvent, + override val interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : RoleSelectMenuContext(component, event, cache), PublicInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/RoleSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/RoleSelectMenu.kt index e100271562..503ca4b603 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/RoleSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/RoleSelectMenu.kt @@ -13,14 +13,14 @@ import dev.kord.rest.builder.component.ActionRowBuilder /** Interface for role select menus. **/ public interface RoleSelectMenu { - /** Apply the role select menu to an action row builder. **/ - public fun applyRoleSelectMenu(selectMenu: SelectMenu<*, *>, builder: ActionRowBuilder) { - if (selectMenu.maximumChoices == null) selectMenu.maximumChoices = OPTIONS_MAX + /** Apply the role select menu to an action row builder. **/ + public fun applyRoleSelectMenu(selectMenu: SelectMenu<*, *>, builder: ActionRowBuilder) { + if (selectMenu.maximumChoices == null) selectMenu.maximumChoices = OPTIONS_MAX - builder.roleSelect(selectMenu.id) { - this.allowedValues = selectMenu.minimumChoices..selectMenu.maximumChoices!! - this.placeholder = selectMenu.placeholder - this.disabled = selectMenu.disabled - } - } + builder.roleSelect(selectMenu.id) { + this.allowedValues = selectMenu.minimumChoices..selectMenu.maximumChoices!! + this.placeholder = selectMenu.placeholder + this.disabled = selectMenu.disabled + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/RoleSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/RoleSelectMenuContext.kt index a23d651666..0865ebf4d7 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/RoleSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/role/RoleSelectMenuContext.kt @@ -17,19 +17,19 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Abstract class representing the execution context of a role select (dropdown) menu component. **/ public abstract class RoleSelectMenuContext( - component: SelectMenu<*, *>, - event: SelectMenuInteractionCreateEvent, - cache: MutableStringKeyedMap, + component: SelectMenu<*, *>, + event: SelectMenuInteractionCreateEvent, + cache: MutableStringKeyedMap, ) : SelectMenuContext(component, event, cache) { - /** Menu options that were selected by the user before de-focusing the menu. **/ - @OptIn(KordUnsafe::class, KordExperimental::class) - public val selected: List by lazy { - if (event.interaction.data.guildId.value == null) { - error("A role select menu cannot be used outside guilds.") - } else { - event.interaction.values.map { - event.kord.unsafe.role(event.interaction.data.guildId.value!!, Snowflake(it)) - } - } - } + /** Menu options that were selected by the user before de-focusing the menu. **/ + @OptIn(KordUnsafe::class, KordExperimental::class) + public val selected: List by lazy { + if (event.interaction.data.guildId.value == null) { + error("A role select menu cannot be used outside guilds.") + } else { + event.interaction.values.map { + event.kord.unsafe.role(event.interaction.data.guildId.value!!, Snowflake(it)) + } + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/EphemeralStringSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/EphemeralStringSelectMenu.kt index 012dbce302..b3ca429428 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/EphemeralStringSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/EphemeralStringSelectMenu.kt @@ -17,22 +17,22 @@ import dev.kord.rest.builder.component.SelectOptionBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialEphemeralSelectMenuResponseBuilder = - (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? /** Class representing an ephemeral-only string select (dropdown) menu. **/ public open class EphemeralStringSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : EphemeralSelectMenu, M>(timeoutTask), StringSelectMenu { - override val options: MutableList = mutableListOf() + override val options: MutableList = mutableListOf() - override fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): EphemeralStringSelectMenuContext = EphemeralStringSelectMenuContext( - this, event, interactionResponse, cache - ) + override fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): EphemeralStringSelectMenuContext = EphemeralStringSelectMenuContext( + this, event, interactionResponse, cache + ) - override fun apply(builder: ActionRowBuilder): Unit = applyStringSelectMenu(this, builder) + override fun apply(builder: ActionRowBuilder): Unit = applyStringSelectMenu(this, builder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/EphemeralStringSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/EphemeralStringSelectMenuContext.kt index a533bc5883..d2ee3c8fe7 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/EphemeralStringSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/EphemeralStringSelectMenuContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing the execution context for an ephemeral-only string select (dropdown) menu. **/ public class EphemeralStringSelectMenuContext( - override val component: EphemeralStringSelectMenu, - override val event: SelectMenuInteractionCreateEvent, - override val interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val component: EphemeralStringSelectMenu, + override val event: SelectMenuInteractionCreateEvent, + override val interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : StringSelectMenuContext(component, event, cache), EphemeralInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/PublicStringSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/PublicStringSelectMenu.kt index 28083c12fb..5e77c15618 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/PublicStringSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/PublicStringSelectMenu.kt @@ -17,22 +17,22 @@ import dev.kord.rest.builder.component.SelectOptionBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialPublicSelectMenuResponseBuilder = - (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? /** Class representing a public-only string select (dropdown) menu. **/ public open class PublicStringSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : PublicSelectMenu, M>(timeoutTask), StringSelectMenu { - override val options: MutableList = mutableListOf() + override val options: MutableList = mutableListOf() - override fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): PublicStringSelectMenuContext = PublicStringSelectMenuContext( - this, event, interactionResponse, cache - ) + override fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): PublicStringSelectMenuContext = PublicStringSelectMenuContext( + this, event, interactionResponse, cache + ) - override fun apply(builder: ActionRowBuilder): Unit = applyStringSelectMenu(this, builder) + override fun apply(builder: ActionRowBuilder): Unit = applyStringSelectMenu(this, builder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/PublicStringSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/PublicStringSelectMenuContext.kt index 16a124d5e7..81528cfcf8 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/PublicStringSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/PublicStringSelectMenuContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing the execution context for a public-only string select (dropdown) menu. **/ public class PublicStringSelectMenuContext( - override val component: PublicStringSelectMenu, - override val event: SelectMenuInteractionCreateEvent, - override val interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val component: PublicStringSelectMenu, + override val event: SelectMenuInteractionCreateEvent, + override val interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : StringSelectMenuContext(component, event, cache), PublicInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/StringSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/StringSelectMenu.kt index b5da22afb8..855877925f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/StringSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/StringSelectMenu.kt @@ -11,74 +11,61 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.component.SelectOptionBuilder import dev.kord.rest.builder.component.StringSelectBuilder -/** Maximum length for an option's description. **/ -public const val DESCRIPTION_MAX: Int = 100 - -/** Maximum length for an option's label. **/ -public const val LABEL_MAX: Int = 100 - -/** Maximum length for an option's value. **/ -public const val VALUE_MAX: Int = 100 - /** Interface for string select menus. **/ public interface StringSelectMenu { - /** List of options for the user to choose from. **/ - public val options: MutableList - - /** Add an option to this select menu. **/ - @Suppress("UnnecessaryParentheses") - public suspend fun option( - label: String, - value: String, - - // TODO: Check this is fixed in later versions of the compiler - // This is nullable like this due to a compiler bug: https://youtrack.jetbrains.com/issue/KT-51820 - body: (suspend SelectOptionBuilder.() -> Unit)? = null, - ) { - val builder = SelectOptionBuilder(label, value) - - if (body != null) { - body(builder) - } - - if ((builder.description?.length ?: 0) > DESCRIPTION_MAX) { - error("Option descriptions must not be longer than $DESCRIPTION_MAX characters.") - } - - if (builder.label.length > LABEL_MAX) { - error("Option labels must not be longer than $LABEL_MAX characters.") - } - - if (builder.value.length > VALUE_MAX) { - error("Option values must not be longer than $VALUE_MAX characters.") - } - - options.add(builder) - } - - /** Apply the string select menu to an action row builder. **/ - public fun applyStringSelectMenu(selectMenu: SelectMenu<*, *>, builder: ActionRowBuilder) { - if (selectMenu.maximumChoices == null || selectMenu.maximumChoices!! > options.size) { - selectMenu.maximumChoices = options.size - } - - builder.stringSelect(selectMenu.id) { - this.allowedValues = selectMenu.minimumChoices..selectMenu.maximumChoices!! - ((this as? StringSelectBuilder)?.options ?: mutableListOf()).addAll(this@StringSelectMenu.options) - this.placeholder = selectMenu.placeholder - this.disabled = selectMenu.disabled - } - } - - /** Validate the options of the string select menu. **/ - @Suppress("UnnecessaryParentheses") - public fun validateOptions() { - if (this.options.isEmpty()) { - error("Menu components must have at least one option.") - } - - if (this.options.size > OPTIONS_MAX) { - error("Menu components must not have more than $OPTIONS_MAX options.") - } - } + /** List of options for the user to choose from. **/ + public val options: MutableList + + /** Add an option to this select menu. **/ + @Suppress("UnnecessaryParentheses") + public suspend fun option( + label: String, + value: String, + + body: (suspend SelectOptionBuilder.() -> Unit) = {}, + ) { + val builder = SelectOptionBuilder(label, value) + + body(builder) + + if ((builder.description?.length ?: 0) > DESCRIPTION_MAX) { + error("Option descriptions must not be longer than $DESCRIPTION_MAX characters.") + } + + if (builder.label.length > LABEL_MAX) { + error("Option labels must not be longer than $LABEL_MAX characters.") + } + + if (builder.value.length > VALUE_MAX) { + error("Option values must not be longer than $VALUE_MAX characters.") + } + + options.add(builder) + } + + /** Apply the string select menu to an action row builder. **/ + public fun applyStringSelectMenu(selectMenu: SelectMenu<*, *>, builder: ActionRowBuilder) { + if (selectMenu.maximumChoices == null || selectMenu.maximumChoices!! > options.size) { + selectMenu.maximumChoices = options.size + } + + builder.stringSelect(selectMenu.id) { + this.allowedValues = selectMenu.minimumChoices..selectMenu.maximumChoices!! + ((this as? StringSelectBuilder)?.options ?: mutableListOf()).addAll(this@StringSelectMenu.options) + this.placeholder = selectMenu.placeholder + this.disabled = selectMenu.disabled + } + } + + /** Validate the options of the string select menu. **/ + @Suppress("UnnecessaryParentheses") + public fun validateOptions() { + if (this.options.isEmpty()) { + error("Menu components must have at least one option.") + } + + if (this.options.size > OPTIONS_MAX) { + error("Menu components must not have more than $OPTIONS_MAX options.") + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/StringSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/StringSelectMenuContext.kt index 77c5b24b87..8d9dcb80cf 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/StringSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/string/StringSelectMenuContext.kt @@ -13,10 +13,10 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Abstract class representing the execution context of a string select (dropdown) menu component. **/ public abstract class StringSelectMenuContext( - component: SelectMenu<*, *>, - event: SelectMenuInteractionCreateEvent, - cache: MutableStringKeyedMap, + component: SelectMenu<*, *>, + event: SelectMenuInteractionCreateEvent, + cache: MutableStringKeyedMap, ) : SelectMenuContext(component, event, cache) { - /** Menu options that were selected by the user before de-focusing the menu. **/ - public val selected: List by lazy { event.interaction.values } + /** Menu options that were selected by the user before de-focusing the menu. **/ + public val selected: List by lazy { event.interaction.values } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/EphemeralUserSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/EphemeralUserSelectMenu.kt index b3a7384d69..bd5e22a416 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/EphemeralUserSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/EphemeralUserSelectMenu.kt @@ -16,20 +16,20 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialEphemeralSelectMenuResponseBuilder = - (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? /** Class representing an ephemeral-only user select (dropdown) menu. **/ public open class EphemeralUserSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : EphemeralSelectMenu, M>(timeoutTask), UserSelectMenu { - override fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): EphemeralUserSelectMenuContext = EphemeralUserSelectMenuContext( - this, event, interactionResponse, cache - ) + override fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): EphemeralUserSelectMenuContext = EphemeralUserSelectMenuContext( + this, event, interactionResponse, cache + ) - override fun apply(builder: ActionRowBuilder): Unit = applyUserSelectMenu(this, builder) + override fun apply(builder: ActionRowBuilder): Unit = applyUserSelectMenu(this, builder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/EphemeralUserSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/EphemeralUserSelectMenuContext.kt index eeaaca34ac..a129b762e2 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/EphemeralUserSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/EphemeralUserSelectMenuContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing the execution context for an ephemeral-only user select (dropdown) menu. **/ public class EphemeralUserSelectMenuContext( - override val component: EphemeralUserSelectMenu, - override val event: SelectMenuInteractionCreateEvent, - override val interactionResponse: EphemeralMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val component: EphemeralUserSelectMenu, + override val event: SelectMenuInteractionCreateEvent, + override val interactionResponse: EphemeralMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : UserSelectMenuContext(component, event, cache), EphemeralInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/PublicUserSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/PublicUserSelectMenu.kt index dbdb77699b..8ebb3c6eb5 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/PublicUserSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/PublicUserSelectMenu.kt @@ -16,20 +16,20 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder public typealias InitialPublicSelectMenuResponseBuilder = - (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? + (suspend InteractionResponseCreateBuilder.(SelectMenuInteractionCreateEvent) -> Unit)? /** Class representing a public-only user select (dropdown) menu. **/ public open class PublicUserSelectMenu( - timeoutTask: Task?, - public override val modal: (() -> M)? = null, + timeoutTask: Task?, + public override val modal: (() -> M)? = null, ) : PublicSelectMenu, M>(timeoutTask), UserSelectMenu { - override fun createContext( - event: SelectMenuInteractionCreateEvent, - interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, - ): PublicUserSelectMenuContext = PublicUserSelectMenuContext( - this, event, interactionResponse, cache - ) + override fun createContext( + event: SelectMenuInteractionCreateEvent, + interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, + ): PublicUserSelectMenuContext = PublicUserSelectMenuContext( + this, event, interactionResponse, cache + ) - override fun apply(builder: ActionRowBuilder): Unit = applyUserSelectMenu(this, builder) + override fun apply(builder: ActionRowBuilder): Unit = applyUserSelectMenu(this, builder) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/PublicUserSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/PublicUserSelectMenuContext.kt index 7f0bbab6ad..c55fff2b25 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/PublicUserSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/PublicUserSelectMenuContext.kt @@ -14,8 +14,8 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Class representing the execution context for a public-only user select (dropdown) menu. **/ public class PublicUserSelectMenuContext( - override val component: PublicUserSelectMenu, - override val event: SelectMenuInteractionCreateEvent, - override val interactionResponse: PublicMessageInteractionResponseBehavior, - cache: MutableStringKeyedMap, + override val component: PublicUserSelectMenu, + override val event: SelectMenuInteractionCreateEvent, + override val interactionResponse: PublicMessageInteractionResponseBehavior, + cache: MutableStringKeyedMap, ) : UserSelectMenuContext(component, event, cache), PublicInteractionContext diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/UserSelectMenu.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/UserSelectMenu.kt index 42333c981b..0164f27f27 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/UserSelectMenu.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/UserSelectMenu.kt @@ -13,14 +13,14 @@ import dev.kord.rest.builder.component.ActionRowBuilder /** Interface for user select menus. **/ public interface UserSelectMenu { - /** Apply the user select menu to an action row builder. **/ - public fun applyUserSelectMenu(selectMenu: SelectMenu<*, *>, builder: ActionRowBuilder) { - if (selectMenu.maximumChoices == null) selectMenu.maximumChoices = OPTIONS_MAX + /** Apply the user select menu to an action row builder. **/ + public fun applyUserSelectMenu(selectMenu: SelectMenu<*, *>, builder: ActionRowBuilder) { + if (selectMenu.maximumChoices == null) selectMenu.maximumChoices = OPTIONS_MAX - builder.userSelect(selectMenu.id) { - this.allowedValues = selectMenu.minimumChoices..selectMenu.maximumChoices!! - this.placeholder = selectMenu.placeholder - this.disabled = selectMenu.disabled - } - } + builder.userSelect(selectMenu.id) { + this.allowedValues = selectMenu.minimumChoices..selectMenu.maximumChoices!! + this.placeholder = selectMenu.placeholder + this.disabled = selectMenu.disabled + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/UserSelectMenuContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/UserSelectMenuContext.kt index c8ed620294..fc9abdd8d7 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/UserSelectMenuContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/menus/user/UserSelectMenuContext.kt @@ -17,13 +17,13 @@ import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent /** Abstract class representing the execution context of a user select (dropdown) menu component. **/ public abstract class UserSelectMenuContext( - component: SelectMenu<*, *>, - event: SelectMenuInteractionCreateEvent, - cache: MutableStringKeyedMap, + component: SelectMenu<*, *>, + event: SelectMenuInteractionCreateEvent, + cache: MutableStringKeyedMap, ) : SelectMenuContext(component, event, cache) { - /** Menu options that were selected by the user before de-focusing the menu. **/ - @OptIn(KordUnsafe::class, KordExperimental::class) - public val selected: List by lazy { - event.interaction.values.map { event.kord.unsafe.user(Snowflake(it)) } - } + /** Menu options that were selected by the user before de-focusing the menu. **/ + @OptIn(KordUnsafe::class, KordExperimental::class) + public val selected: List by lazy { + event.interaction.values.map { event.kord.unsafe.user(Snowflake(it)) } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/types/HasPartialEmoji.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/types/HasPartialEmoji.kt index 9cf973a5c4..70d6224c4a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/types/HasPartialEmoji.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/components/types/HasPartialEmoji.kt @@ -16,48 +16,48 @@ import dev.kord.core.entity.ReactionEmoji * function DRY. */ public interface HasPartialEmoji { - /** - * A partial emoji object, either a guild or Unicode emoji. Optional if you've got a label. - * - * @see emoji - */ - public var partialEmoji: DiscordPartialEmoji? + /** + * A partial emoji object, either a guild or Unicode emoji. Optional if you've got a label. + * + * @see emoji + */ + public var partialEmoji: DiscordPartialEmoji? } /** Convenience function for setting [HasPartialEmoji.partialEmoji] based on a given Unicode emoji. **/ public fun HasPartialEmoji.emoji(unicodeEmoji: String) { - partialEmoji = DiscordPartialEmoji( - name = unicodeEmoji - ) + partialEmoji = DiscordPartialEmoji( + name = unicodeEmoji + ) } /** Convenience function for setting [HasPartialEmoji.partialEmoji] based on a given guild custom emoji. **/ public fun HasPartialEmoji.emoji(guildEmoji: GuildEmoji) { - partialEmoji = DiscordPartialEmoji( - id = guildEmoji.id, - name = guildEmoji.name, - animated = guildEmoji.isAnimated.optional() - ) + partialEmoji = DiscordPartialEmoji( + id = guildEmoji.id, + name = guildEmoji.name, + animated = guildEmoji.isAnimated.optional() + ) } /** Convenience function for setting [HasPartialEmoji.partialEmoji] based on a given reaction emoji. **/ public fun HasPartialEmoji.emoji(unicodeEmoji: ReactionEmoji.Unicode) { - partialEmoji = DiscordPartialEmoji( - name = unicodeEmoji.name - ) + partialEmoji = DiscordPartialEmoji( + name = unicodeEmoji.name + ) } /** Convenience function for setting [HasPartialEmoji.partialEmoji] based on a given reaction emoji. **/ public fun HasPartialEmoji.emoji(guildEmoji: ReactionEmoji.Custom) { - partialEmoji = DiscordPartialEmoji( - id = guildEmoji.id, - name = guildEmoji.name, - animated = guildEmoji.isAnimated.optional() - ) + partialEmoji = DiscordPartialEmoji( + id = guildEmoji.id, + name = guildEmoji.name, + animated = guildEmoji.isAnimated.optional() + ) } /** Convenience function for setting [HasPartialEmoji.partialEmoji] based on a given reaction emoji. **/ public fun HasPartialEmoji.emoji(emoji: ReactionEmoji): Unit = when (emoji) { - is ReactionEmoji.Unicode -> emoji(emoji) - is ReactionEmoji.Custom -> emoji(emoji) + is ReactionEmoji.Unicode -> emoji(emoji) + is ReactionEmoji.Custom -> emoji(emoji) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/EventContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/EventContext.kt index 1597d60e8c..9450a3c264 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/EventContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/EventContext.kt @@ -28,78 +28,78 @@ import java.util.* * @param cache Data cache map shared with the defined checks. */ public open class EventContext( - public open val eventHandler: EventHandler, - public open val event: T, - public open val cache: MutableStringKeyedMap, + public open val eventHandler: EventHandler, + public open val event: T, + public open val cache: MutableStringKeyedMap, ) : KordExKoinComponent, TranslatableContext { - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by lazy { getTranslationProvider() } + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by lazy { getTranslationProvider() } - /** Current Sentry context, containing breadcrumbs and other goodies. **/ - public val sentry: SentryContext = SentryContext() + /** Current Sentry context, containing breadcrumbs and other goodies. **/ + public val sentry: SentryContext = SentryContext() - override var resolvedLocale: Locale? = null + override var resolvedLocale: Locale? = null - override val bundle: String? - get() = eventHandler.extension.bundle + override val bundle: String? + get() = eventHandler.extension.bundle - /** - * Given a translation key and optional bundle name, return the translation for the locale provided by the bot's - * configured locale resolvers. - */ - public override suspend fun translate( - key: String, - bundleName: String?, - replacements: Array, - ): String { - val locale: Locale = getLocale() + /** + * Given a translation key and optional bundle name, return the translation for the locale provided by the bot's + * configured locale resolvers. + */ + public override suspend fun translate( + key: String, + bundleName: String?, + replacements: Array, + ): String { + val locale: Locale = getLocale() - return translationsProvider.translate(key, locale, bundleName, replacements) - } + return translationsProvider.translate(key, locale, bundleName, replacements) + } - /** - * Given a translation key and optional bundle name, return the translation for the locale provided by the bot's - * configured locale resolvers. - */ - public override suspend fun translate( - key: String, - bundleName: String?, - replacements: Map, - ): String { - val locale: Locale = getLocale() + /** + * Given a translation key and optional bundle name, return the translation for the locale provided by the bot's + * configured locale resolvers. + */ + public override suspend fun translate( + key: String, + bundleName: String?, + replacements: Map, + ): String { + val locale: Locale = getLocale() - return translationsProvider.translate(key, locale, bundleName, replacements) - } + return translationsProvider.translate(key, locale, bundleName, replacements) + } - override suspend fun getLocale(): Locale { - var locale = resolvedLocale + override suspend fun getLocale(): Locale { + var locale = resolvedLocale - if (locale != null) { - return locale - } + if (locale != null) { + return locale + } - val eventObj = event as Event + val eventObj = event as Event - val guild = guildFor(eventObj) - val channel = channelFor(eventObj) - val user = userFor(eventObj) + val guild = guildFor(eventObj) + val channel = channelFor(eventObj) + val user = userFor(eventObj) - for (resolver in eventHandler.extension.bot.settings.i18nBuilder.localeResolvers) { - val result = resolver(guild, channel, user, interactionFor(eventObj)) + for (resolver in eventHandler.extension.bot.settings.i18nBuilder.localeResolvers) { + val result = resolver(guild, channel, user, interactionFor(eventObj)) - if (result != null) { - locale = result + if (result != null) { + locale = result - break - } - } + break + } + } - if (locale == null) { - locale = eventHandler.extension.bot.settings.i18nBuilder.defaultLocale - } + if (locale == null) { + locale = eventHandler.extension.bot.settings.i18nBuilder.defaultLocale + } - resolvedLocale = locale + resolvedLocale = locale - return locale - } + return locale + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/EventHandler.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/EventHandler.kt index a93bbba240..05087536cf 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/EventHandler.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/EventHandler.kt @@ -28,8 +28,8 @@ import java.util.* private val logger = KotlinLogging.logger {} private val defaultLocale: Locale - get() = - getKoin().get().i18nBuilder.defaultLocale + get() = + getKoin().get().i18nBuilder.defaultLocale /** * Class representing an event handler. Event handlers react to a given Kord event. @@ -41,110 +41,110 @@ private val defaultLocale: Locale * @param extension The [Extension] that registered this event handler. */ public open class EventHandler( - public val extension: Extension + public val extension: Extension, ) : KordExKoinComponent { - /** Sentry adapter, for easy access to Sentry functions. **/ - public val sentry: SentryAdapter by inject() - - /** Current Kord instance powering the bot. **/ - public open val kord: Kord by inject() - - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by inject() - - /** - * @suppress - */ - public lateinit var body: suspend EventContext.() -> Unit - - /** - * @suppress - */ - public val checkList: MutableList> = mutableListOf() - - /** - * @suppress This is the job returned by `Kord#on`, which we cancel to stop listening. - */ - public var job: Job? = null - - /** @suppress Internal hack to work around logic ordering with inline functions. **/ - public var listenerRegistrationCallable: (() -> Unit)? = null - - /** Cached locale variable, stored and retrieved by [getLocale]. **/ - public var resolvedLocale: Locale? = null - - /** - * An internal function used to ensure that all of an event handler's required arguments are present. - * - * @throws InvalidEventHandlerException Thrown when a required argument hasn't been set. - */ - @Throws(InvalidEventHandlerException::class) - public fun validate() { - if (!::body.isInitialized) { - throw InvalidEventHandlerException("No event handler action given.") - } - } - - // region: DSL functions - - /** - * Define what will happen when your event handler is invoked. - * - * @param action The body of your event handler, which will be executed when it is invoked. - */ - public fun action(action: suspend EventContext.() -> Unit) { - this.body = action - } - - /** - * Define a check which must pass for the event handler to be executed. - * - * An event handler may have multiple checks - all checks must pass for the event handler to be executed. - * Checks will be run in the order that they're defined. - * - * This function can be used DSL-style with a given body, or it can be passed one or more - * predefined functions. See the samples for more information. - * - * @param checks Checks to apply to this event handler. - */ - public fun check(vararg checks: CheckWithCache): Unit = checks.forEach { checkList.add(it) } - - /** - * Overloaded check function to allow for DSL syntax. - * - * @param check Check to apply to this event handler. - */ - public fun check(check: CheckWithCache): Boolean = checkList.add(check) - - // endregion - - /** - * Execute this event handler, given an event. - * - * This function takes an event of type T and executes the [event handler body][action], assuming all checks pass. - * - * If an exception is thrown by the [event handler body][action], it is caught and a traceback - * is printed. - * - * @param event The given event object. - */ - public suspend fun call(event: T) { - val cache: MutableStringKeyedMap = mutableMapOf() - - for (check in checkList) { - val context = CheckContextWithCache(event, defaultLocale, cache) - - check(context) - - if (!context.passed) { - return // We don't send responses to plain event handlers - } - } - - val context = EventContext(this, event, cache) - val eventName = event::class.simpleName - - if (sentry.enabled) { + /** Sentry adapter, for easy access to Sentry functions. **/ + public val sentry: SentryAdapter by inject() + + /** Current Kord instance powering the bot. **/ + public open val kord: Kord by inject() + + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by inject() + + /** + * @suppress + */ + public lateinit var body: suspend EventContext.() -> Unit + + /** + * @suppress + */ + public val checkList: MutableList> = mutableListOf() + + /** + * @suppress This is the job returned by `Kord#on`, which we cancel to stop listening. + */ + public var job: Job? = null + + /** @suppress Internal hack to work around logic ordering with inline functions. **/ + public var listenerRegistrationCallable: (() -> Unit)? = null + + /** Cached locale variable, stored and retrieved by [getLocale]. **/ + public var resolvedLocale: Locale? = null + + /** + * An internal function used to ensure that all of an event handler's required arguments are present. + * + * @throws InvalidEventHandlerException Thrown when a required argument hasn't been set. + */ + @Throws(InvalidEventHandlerException::class) + public fun validate() { + if (!::body.isInitialized) { + throw InvalidEventHandlerException("No event handler action given.") + } + } + + // region: DSL functions + + /** + * Define what will happen when your event handler is invoked. + * + * @param action The body of your event handler, which will be executed when it is invoked. + */ + public fun action(action: suspend EventContext.() -> Unit) { + this.body = action + } + + /** + * Define a check which must pass for the event handler to be executed. + * + * An event handler may have multiple checks - all checks must pass for the event handler to be executed. + * Checks will be run in the order that they're defined. + * + * This function can be used DSL-style with a given body, or it can be passed one or more + * predefined functions. See the samples for more information. + * + * @param checks Checks to apply to this event handler. + */ + public fun check(vararg checks: CheckWithCache): Unit = checks.forEach { checkList.add(it) } + + /** + * Overloaded check function to allow for DSL syntax. + * + * @param check Check to apply to this event handler. + */ + public fun check(check: CheckWithCache): Boolean = checkList.add(check) + + // endregion + + /** + * Execute this event handler, given an event. + * + * This function takes an event of type T and executes the [event handler body][action], assuming all checks pass. + * + * If an exception is thrown by the [event handler body][action], it is caught and a traceback + * is printed. + * + * @param event The given event object. + */ + public suspend fun call(event: T) { + val cache: MutableStringKeyedMap = mutableMapOf() + + for (check in checkList) { + val context = CheckContextWithCache(event, defaultLocale, cache) + + check(context) + + if (!context.passed) { + return // We don't send responses to plain event handlers + } + } + + val context = EventContext(this, event, cache) + val eventName = event::class.simpleName + + if (sentry.enabled) { context.sentry.context( "event", eventName ?: "Unknown", ) @@ -153,9 +153,9 @@ public open class EventHandler( "extension", extension.name ) - context.sentry.breadcrumb(BreadcrumbType.Info) { - val messageBehavior = messageFor(event) - val thread = threadFor(event)?.asChannel() + context.sentry.breadcrumb(BreadcrumbType.Info) { + val messageBehavior = messageFor(event) + val thread = threadFor(event)?.asChannel() val roleBehavior = roleFor(event) category = "event" @@ -166,108 +166,108 @@ public open class EventHandler( role = roleBehavior?.guild?.getRoleOrNull(roleBehavior.id) user = userFor(event)?.asUserOrNull() - if (messageBehavior != null) { - data["message.id"] = messageBehavior.id.toString() - } + if (messageBehavior != null) { + data["message.id"] = messageBehavior.id.toString() + } if (thread != null) { data["channel.thread.id"] = thread.id.toString() data["channel.thread.name"] = thread.name } - } - } + } + } - @Suppress("TooGenericExceptionCaught") // Anything could happen here - try { - this.body(context) - } catch (t: Throwable) { - if (sentry.enabled) { - logger.trace { "Submitting error to sentry." } + @Suppress("TooGenericExceptionCaught") // Anything could happen here + try { + this.body(context) + } catch (t: Throwable) { + if (sentry.enabled) { + logger.trace { "Submitting error to sentry." } - val sentryId = context.sentry.captureThrowable(t) + val sentryId = context.sentry.captureThrowable(t) if (sentryId != null) { logger.info { "Error submitted to Sentry: $sentryId" } } - logger.error(t) { "Error during execution of event handler ($eventName)" } - } else { - logger.error(t) { "Error during execution of event handler ($eventName)" } - } - } - } - - /** Resolve the locale for the given event. **/ - public suspend fun Event.getLocale(): Locale { - var locale: Locale? = resolvedLocale - - if (locale != null) { - return locale - } - - val guild = guildFor(this) - val channel = channelFor(this) - val user = userFor(this) - - for (resolver in extension.bot.settings.i18nBuilder.localeResolvers) { - val result = resolver(guild, channel, user, interactionFor(this)) - - if (result != null) { - locale = result - break - } - } - - resolvedLocale = locale ?: extension.bot.settings.i18nBuilder.defaultLocale - - return resolvedLocale!! - } - - /** - * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured - * locale resolvers. - */ - public suspend fun Event.translate( - key: String, - bundleName: String?, - replacements: Array = arrayOf() - ): String { - val locale = getLocale() - - return translationsProvider.translate(key, locale, bundleName, replacements) - } - - /** - * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured - * locale resolvers. - */ - public suspend fun Event.translate( - key: String, - bundleName: String?, - replacements: Map - ): String { - val locale = getLocale() - - return translationsProvider.translate(key, locale, bundleName, replacements) - } - - /** - * Given a translation key and possible replacements, return the translation for the given locale in the - * extension's configured bundle, for the locale provided by the bot's configured locale resolvers. - */ - public suspend fun Event.translate(key: String, replacements: Array = arrayOf()): String = translate( - key, - extension.bundle, - replacements - ) - - /** - * Given a translation key and possible replacements, return the translation for the given locale in the - * extension's configured bundle, for the locale provided by the bot's configured locale resolvers. - */ - public suspend fun Event.translate(key: String, replacements: Map): String = translate( - key, - extension.bundle, - replacements - ) + logger.error(t) { "Error during execution of event handler ($eventName)" } + } else { + logger.error(t) { "Error during execution of event handler ($eventName)" } + } + } + } + + /** Resolve the locale for the given event. **/ + public suspend fun Event.getLocale(): Locale { + var locale: Locale? = resolvedLocale + + if (locale != null) { + return locale + } + + val guild = guildFor(this) + val channel = channelFor(this) + val user = userFor(this) + + for (resolver in extension.bot.settings.i18nBuilder.localeResolvers) { + val result = resolver(guild, channel, user, interactionFor(this)) + + if (result != null) { + locale = result + break + } + } + + resolvedLocale = locale ?: extension.bot.settings.i18nBuilder.defaultLocale + + return resolvedLocale!! + } + + /** + * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured + * locale resolvers. + */ + public suspend fun Event.translate( + key: String, + bundleName: String?, + replacements: Array = arrayOf(), + ): String { + val locale = getLocale() + + return translationsProvider.translate(key, locale, bundleName, replacements) + } + + /** + * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured + * locale resolvers. + */ + public suspend fun Event.translate( + key: String, + bundleName: String?, + replacements: Map, + ): String { + val locale = getLocale() + + return translationsProvider.translate(key, locale, bundleName, replacements) + } + + /** + * Given a translation key and possible replacements, return the translation for the given locale in the + * extension's configured bundle, for the locale provided by the bot's configured locale resolvers. + */ + public suspend fun Event.translate(key: String, replacements: Array = arrayOf()): String = translate( + key, + extension.bundle, + replacements + ) + + /** + * Given a translation key and possible replacements, return the translation for the given locale in the + * extension's configured bundle, for the locale provided by the bot's configured locale resolvers. + */ + public suspend fun Event.translate(key: String, replacements: Map): String = translate( + key, + extension.bundle, + replacements + ) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/ExtensionStateEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/ExtensionStateEvent.kt index 2d24d7be4e..b42fe60648 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/ExtensionStateEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/ExtensionStateEvent.kt @@ -16,6 +16,6 @@ import com.kotlindiscord.kord.extensions.extensions.ExtensionState * @property state Extension's new state */ public data class ExtensionStateEvent( - public val extension: Extension, - public val state: ExtensionState + public val extension: Extension, + public val state: ExtensionState, ) : KordExEvent diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/KordExEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/KordExEvent.kt index 8fe773f96c..541218c992 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/KordExEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/KordExEvent.kt @@ -16,10 +16,10 @@ import dev.kord.core.event.Event * Base interface for events fired by Kord Extensions. */ public interface KordExEvent : Event, KordExKoinComponent { - override val kord: Kord get() = getKoin().get() - override val shard: Int get() = -1 + override val kord: Kord get() = getKoin().get() + override val shard: Int get() = -1 - @KordPreview - override val customContext: MutableStringKeyedMap - get() = mutableMapOf() + @KordPreview + override val customContext: MutableStringKeyedMap + get() = mutableMapOf() } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/ModalInteractionCompleteEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/ModalInteractionCompleteEvent.kt index 75a2e4fe23..6889402555 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/ModalInteractionCompleteEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/ModalInteractionCompleteEvent.kt @@ -15,6 +15,6 @@ import dev.kord.core.entity.interaction.ModalSubmitInteraction * @param interaction Interaction object provided by the corresponding event. */ public class ModalInteractionCompleteEvent( - public val id: String, - public val interaction: ModalSubmitInteraction + public val id: String, + public val interaction: ModalSubmitInteraction, ) : KordExEvent diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/GuildJoinRequestDeleteEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/GuildJoinRequestDeleteEvent.kt index 3f174df3c6..26046ac3f8 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/GuildJoinRequestDeleteEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/GuildJoinRequestDeleteEvent.kt @@ -26,28 +26,28 @@ import dev.kord.core.supplier.EntitySupplyStrategy @OptIn(KordUnsafe::class, KordExperimental::class) public class GuildJoinRequestDeleteEvent( - public val data: GuildJoinRequestDelete, + public val data: GuildJoinRequestDelete, - override val kord: Kord = getKoin().get(), - override val supplier: EntitySupplier = kord.defaultSupplier, + override val kord: Kord = getKoin().get(), + override val supplier: EntitySupplier = kord.defaultSupplier, ) : KordExEvent, Strategizable, MemberEvent { - public val guildId: Snowflake get() = data.guildId - public val userId: Snowflake get() = data.userId - public val requestId: Snowflake get() = data.id + public val guildId: Snowflake get() = data.guildId + public val userId: Snowflake get() = data.userId + public val requestId: Snowflake get() = data.id - override val guild: GuildBehavior = kord.unsafe.guild(guildId) - override val user: UserBehavior = kord.unsafe.user(userId) - override val member: MemberBehavior = kord.unsafe.member(guildId, userId) + override val guild: GuildBehavior = kord.unsafe.guild(guildId) + override val user: UserBehavior = kord.unsafe.user(userId) + override val member: MemberBehavior = kord.unsafe.member(guildId, userId) - public override suspend fun getUser(): User = supplier.getUser(userId) - public override suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId) + public override suspend fun getUser(): User = supplier.getUser(userId) + public override suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId) - public override suspend fun getMember(): Member = supplier.getMember(guildId, userId) - public override suspend fun getMemberOrNull(): Member? = supplier.getMemberOrNull(guildId, userId) + public override suspend fun getMember(): Member = supplier.getMember(guildId, userId) + public override suspend fun getMemberOrNull(): Member? = supplier.getMemberOrNull(guildId, userId) - public override suspend fun getGuild(): Guild = supplier.getGuild(guildId) - public override suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId) + public override suspend fun getGuild(): Guild = supplier.getGuild(guildId) + public override suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId) - override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildJoinRequestDeleteEvent = - GuildJoinRequestDeleteEvent(data, supplier = strategy.supply(kord)) + override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildJoinRequestDeleteEvent = + GuildJoinRequestDeleteEvent(data, supplier = strategy.supply(kord)) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/GuildJoinRequestUpdateEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/GuildJoinRequestUpdateEvent.kt index 4692064f0e..ac79a411d2 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/GuildJoinRequestUpdateEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/GuildJoinRequestUpdateEvent.kt @@ -28,30 +28,30 @@ import dev.kord.core.supplier.EntitySupplyStrategy @OptIn(KordUnsafe::class, KordExperimental::class) public class GuildJoinRequestUpdateEvent( - public val data: GuildJoinRequestUpdate, + public val data: GuildJoinRequestUpdate, - override val kord: Kord = getKoin().get(), - override val supplier: EntitySupplier = kord.defaultSupplier, + override val kord: Kord = getKoin().get(), + override val supplier: EntitySupplier = kord.defaultSupplier, ) : KordExEvent, Strategizable, MemberEvent { - public val status: ApplicationStatus get() = data.status - public val guildId: Snowflake get() = data.guildId - public val userId: Snowflake get() = data.request.userId - public val requestId: Snowflake get() = data.request.id - public val request: GuildJoinRequest get() = data.request + public val status: ApplicationStatus get() = data.status + public val guildId: Snowflake get() = data.guildId + public val userId: Snowflake get() = data.request.userId + public val requestId: Snowflake get() = data.request.id + public val request: GuildJoinRequest get() = data.request - override val guild: GuildBehavior = kord.unsafe.guild(guildId) - override val user: UserBehavior = kord.unsafe.user(userId) - override val member: MemberBehavior = kord.unsafe.member(guildId, userId) + override val guild: GuildBehavior = kord.unsafe.guild(guildId) + override val user: UserBehavior = kord.unsafe.user(userId) + override val member: MemberBehavior = kord.unsafe.member(guildId, userId) - public override suspend fun getUser(): User = supplier.getUser(userId) - public override suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId) + public override suspend fun getUser(): User = supplier.getUser(userId) + public override suspend fun getUserOrNull(): User? = supplier.getUserOrNull(userId) - public override suspend fun getMember(): Member = supplier.getMember(guildId, userId) - public override suspend fun getMemberOrNull(): Member? = supplier.getMemberOrNull(guildId, userId) + public override suspend fun getMember(): Member = supplier.getMember(guildId, userId) + public override suspend fun getMemberOrNull(): Member? = supplier.getMemberOrNull(guildId, userId) - public override suspend fun getGuild(): Guild = supplier.getGuild(guildId) - public override suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId) + public override suspend fun getGuild(): Guild = supplier.getGuild(guildId) + public override suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId) - override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildJoinRequestUpdateEvent = - GuildJoinRequestUpdateEvent(data, supplier = strategy.supply(kord)) + override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildJoinRequestUpdateEvent = + GuildJoinRequestUpdateEvent(data, supplier = strategy.supply(kord)) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/ApplicationStatus.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/ApplicationStatus.kt index 122538aa8b..c580c845cd 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/ApplicationStatus.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/ApplicationStatus.kt @@ -11,12 +11,12 @@ import kotlinx.serialization.Serializable @Serializable public enum class ApplicationStatus { - @SerialName("APPROVED") - Approved, + @SerialName("APPROVED") + Approved, - @SerialName("REJECTED") - Rejected, + @SerialName("REJECTED") + Rejected, - @SerialName("SUBMITTED") - Submitted + @SerialName("SUBMITTED") + Submitted } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequest.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequest.kt index 951fd478e8..eb945b8fcb 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequest.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequest.kt @@ -14,40 +14,40 @@ import kotlinx.serialization.Serializable @Serializable public data class GuildJoinRequest( - @SerialName("user_id") - public val userId: Snowflake, + @SerialName("user_id") + public val userId: Snowflake, - @SerialName("rejection_reason") - public val rejectionReason: String?, + @SerialName("rejection_reason") + public val rejectionReason: String?, - @SerialName("guild_id") - public val guildId: Snowflake, + @SerialName("guild_id") + public val guildId: Snowflake, - @SerialName("created_at") - public val createdAt: Instant, + @SerialName("created_at") + public val createdAt: Instant, - @SerialName("application_status") - public val status: ApplicationStatus, + @SerialName("application_status") + public val status: ApplicationStatus, - @SerialName("form_responses") - public val formResponses: List, + @SerialName("form_responses") + public val formResponses: List, - @SerialName("actioned_by_user") - public val actionedByUser: DiscordUser? = null, + @SerialName("actioned_by_user") + public val actionedByUser: DiscordUser? = null, - @SerialName("actioned_at") - public val actionedAtSnowflake: Snowflake? = null, + @SerialName("actioned_at") + public val actionedAtSnowflake: Snowflake? = null, - public val id: Snowflake, - public val user: DiscordUser, + public val id: Snowflake, + public val user: DiscordUser, - // last_seen? + // last_seen? ) { - public val requestBypassed: Boolean by lazy { - user.id == actionedByUser?.id - } + public val requestBypassed: Boolean by lazy { + user.id == actionedByUser?.id + } - public val actionedAt: Instant? by lazy { - actionedAtSnowflake?.timestamp - } + public val actionedAt: Instant? by lazy { + actionedAtSnowflake?.timestamp + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestDelete.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestDelete.kt index 43d52aa1e3..47acd020da 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestDelete.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestDelete.kt @@ -12,11 +12,11 @@ import kotlinx.serialization.Serializable @Serializable public data class GuildJoinRequestDelete( - @SerialName("guild_id") - public val guildId: Snowflake, + @SerialName("guild_id") + public val guildId: Snowflake, - @SerialName("user_id") - public val userId: Snowflake, + @SerialName("user_id") + public val userId: Snowflake, - public val id: Snowflake, + public val id: Snowflake, ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestResponse.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestResponse.kt index a3b548caca..f58dc5ce35 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestResponse.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestResponse.kt @@ -15,38 +15,38 @@ import kotlinx.serialization.json.JsonClassDiscriminator @Serializable @JsonClassDiscriminator("field_type") public sealed class GuildJoinRequestResponse { - public abstract val required: Boolean - public abstract val label: String - - // description, automations? - - @Serializable - @SerialName("TERMS") - public class TermsResponse( - override val required: Boolean, - override val label: String, - - public val response: Boolean, - public val values: List, - ) : GuildJoinRequestResponse() - - @Serializable - @SerialName("PARAGRAPH") - public class ParagraphResponse( - override val required: Boolean, - override val label: String, - - public val placeholder: String?, - public val response: String, - ) : GuildJoinRequestResponse() - - @Serializable - @SerialName("MULTIPLE_CHOICE") - public class MultipleChoiceResponse( - override val required: Boolean, - override val label: String, - - public val response: Int, - public val choices: List, - ) : GuildJoinRequestResponse() + public abstract val required: Boolean + public abstract val label: String + + // description, automations? + + @Serializable + @SerialName("TERMS") + public class TermsResponse( + override val required: Boolean, + override val label: String, + + public val response: Boolean, + public val values: List, + ) : GuildJoinRequestResponse() + + @Serializable + @SerialName("PARAGRAPH") + public class ParagraphResponse( + override val required: Boolean, + override val label: String, + + public val placeholder: String?, + public val response: String, + ) : GuildJoinRequestResponse() + + @Serializable + @SerialName("MULTIPLE_CHOICE") + public class MultipleChoiceResponse( + override val required: Boolean, + override val label: String, + + public val response: Int, + public val choices: List, + ) : GuildJoinRequestResponse() } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestUpdate.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestUpdate.kt index a2a2524c90..7992a4ce5a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestUpdate.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestUpdate.kt @@ -12,9 +12,9 @@ import kotlinx.serialization.Serializable @Serializable public data class GuildJoinRequestUpdate( - public val status: ApplicationStatus, - public val request: GuildJoinRequest, + public val status: ApplicationStatus, + public val request: GuildJoinRequest, - @SerialName("guild_id") - public val guildId: Snowflake, + @SerialName("guild_id") + public val guildId: Snowflake, ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestUser.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestUser.kt index 02f81039e2..ab925c13c8 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestUser.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/extra/models/GuildJoinRequestUser.kt @@ -13,16 +13,16 @@ import kotlinx.serialization.Serializable @Serializable public data class GuildJoinRequestUser( - @SerialName("public_flags") - public val flags: UserFlags, + @SerialName("public_flags") + public val flags: UserFlags, - @SerialName("display_name") - public val displayName: String?, + @SerialName("display_name") + public val displayName: String?, - public val username: String, - public val id: Snowflake, - public val discriminator: Int, - public val avatar: String, + public val username: String, + public val id: Snowflake, + public val discriminator: Int, + public val avatar: String, - // avatar_decoration? + // avatar_decoration? ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/ChannelEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/ChannelEvent.kt index e71367f530..35158acf27 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/ChannelEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/ChannelEvent.kt @@ -11,20 +11,20 @@ import dev.kord.core.entity.channel.Channel /** Generic interface for custom events that can contain channel behaviors. Mostly used by checks. **/ public interface ChannelEvent { - /** The channel behavior for this event, if any. **/ - public val channel: ChannelBehavior? + /** The channel behavior for this event, if any. **/ + public val channel: ChannelBehavior? - /** Get a generic Channel object, or throw if one can't be retrieved. **/ - public suspend fun getChannel(): Channel + /** Get a generic Channel object, or throw if one can't be retrieved. **/ + public suspend fun getChannel(): Channel - /** Get a generic Channel object, or return null if one can't be retrieved. **/ - public suspend fun getChannelOrNull(): Channel? + /** Get a generic Channel object, or return null if one can't be retrieved. **/ + public suspend fun getChannelOrNull(): Channel? } /** Get a channel object of the given type, or throw if one can't be retrieved or cast. **/ public suspend inline fun ChannelEvent.getChannelOf(): T = - getChannel() as T + getChannel() as T /** Get a channel object of the given type, or return null if one can't be retrieved or cast. **/ public suspend inline fun ChannelEvent.getChannelOfOrNull(): T? = - getChannelOrNull() as? T + getChannelOrNull() as? T diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/GuildEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/GuildEvent.kt index 65385caa3f..b884319a92 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/GuildEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/GuildEvent.kt @@ -11,12 +11,12 @@ import dev.kord.core.entity.Guild /** Generic interface for custom events that can contain guild behaviors. Mostly used by checks. **/ public interface GuildEvent { - /** The guild behavior for this event, if any. **/ - public val guild: GuildBehavior? + /** The guild behavior for this event, if any. **/ + public val guild: GuildBehavior? - /** Get a Guild object, or throw if one can't be retrieved. **/ - public suspend fun getGuild(): Guild + /** Get a Guild object, or throw if one can't be retrieved. **/ + public suspend fun getGuild(): Guild - /** Get a Guild object, or return null if one can't be retrieved. **/ - public suspend fun getGuildOrNull(): Guild? + /** Get a Guild object, or return null if one can't be retrieved. **/ + public suspend fun getGuildOrNull(): Guild? } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/MemberEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/MemberEvent.kt index 1980bf3d06..b5195cc07a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/MemberEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/MemberEvent.kt @@ -16,12 +16,12 @@ import dev.kord.core.entity.Member * member object. */ public interface MemberEvent : GuildEvent, UserEvent { - /** The member behavior for this event, if any. **/ - public val member: MemberBehavior? + /** The member behavior for this event, if any. **/ + public val member: MemberBehavior? - /** Get a Member object, or throw if one can't be retrieved. **/ - public suspend fun getMember(): Member + /** Get a Member object, or throw if one can't be retrieved. **/ + public suspend fun getMember(): Member - /** Get a Member object, or return null if one can't be retrieved. **/ - public suspend fun getMemberOrNull(): Member? + /** Get a Member object, or return null if one can't be retrieved. **/ + public suspend fun getMemberOrNull(): Member? } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/MessageEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/MessageEvent.kt index 3fbe561274..5be827bd4e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/MessageEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/MessageEvent.kt @@ -11,12 +11,12 @@ import dev.kord.core.entity.Message /** Generic interface for custom events that can contain message behaviors. Mostly used by checks. **/ public interface MessageEvent { - /** The message behavior for this event, if any. **/ - public val message: MessageBehavior? + /** The message behavior for this event, if any. **/ + public val message: MessageBehavior? - /** Get a Message object, or throw if one can't be retrieved. **/ - public suspend fun getMessage(): Message + /** Get a Message object, or throw if one can't be retrieved. **/ + public suspend fun getMessage(): Message - /** Get a Message object, or return null if one can't be retrieved. **/ - public suspend fun getMessageOrNull(): Message? + /** Get a Message object, or return null if one can't be retrieved. **/ + public suspend fun getMessageOrNull(): Message? } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/RoleEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/RoleEvent.kt index f9cae6277c..d30f50dc12 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/RoleEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/RoleEvent.kt @@ -11,12 +11,12 @@ import dev.kord.core.entity.Role /** Generic interface for custom events that can contain role behaviors. Mostly used by checks. **/ public interface RoleEvent { - /** The role behavior for this event, if any. **/ - public val role: RoleBehavior? + /** The role behavior for this event, if any. **/ + public val role: RoleBehavior? - /** Get a Role object, or throw if one can't be retrieved. **/ - public suspend fun getRole(): Role + /** Get a Role object, or throw if one can't be retrieved. **/ + public suspend fun getRole(): Role - /** Get a Role object, or return null if one can't be retrieved. **/ - public suspend fun getRoleOrNull(): Role? + /** Get a Role object, or return null if one can't be retrieved. **/ + public suspend fun getRoleOrNull(): Role? } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/UserEvent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/UserEvent.kt index c0636a7e45..cbc765902f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/UserEvent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/events/interfaces/UserEvent.kt @@ -11,12 +11,12 @@ import dev.kord.core.entity.User /** Generic interface for custom events that can contain user behaviors. Mostly used by checks. **/ public interface UserEvent { - /** The user behavior for this event, if any. **/ - public val user: UserBehavior? + /** The user behavior for this event, if any. **/ + public val user: UserBehavior? - /** Get a User object, or throw if one can't be retrieved. **/ - public suspend fun getUser(): User + /** Get a User object, or throw if one can't be retrieved. **/ + public suspend fun getUser(): User - /** Get a User object, or return null if one can't be retrieved. **/ - public suspend fun getUserOrNull(): User? + /** Get a User object, or return null if one can't be retrieved. **/ + public suspend fun getUserOrNull(): User? } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/Extension.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/Extension.kt index 4f06bede5a..271d9ecde0 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/Extension.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/Extension.kt @@ -40,202 +40,202 @@ private val logger = KotlinLogging.logger {} * clean and configurable. */ public abstract class Extension : KordExKoinComponent { - /** The [ExtensibleBot] instance that this extension is installed to. **/ - public open val bot: ExtensibleBot by inject() - - /** Current Kord instance powering the bot. **/ - public open val kord: Kord by inject() - - /** Message command registry. **/ - public open val chatCommandRegistry: ChatCommandRegistry by inject() - - /** Slash command registry. **/ - public open val applicationCommandRegistry: ApplicationCommandRegistry by inject() - - /** - * The name of this extension. - * - * Ensure you override this in your extension. This should be a unique name that can later - * be used to refer to your specific extension after it's been registered. - */ - public abstract val name: String - - /** - * The current loading/unloading state of the extension. - */ - public open var state: ExtensionState = ExtensionState.UNLOADED - - /** Check whether this extension's state is [ExtensionState.LOADED]. **/ - public open val loaded: Boolean get() = state == ExtensionState.LOADED - - /** - * List of registered event handlers. - * - * When an extension is unloaded, all the event handlers are cancelled and - * removed from the bot. - */ - public open val eventHandlers: MutableList> = mutableListOf() - - /** - * List of registered commands. - * - * When an extension is unloaded, all the commands are removed from the bot. - */ - public open val chatCommands: MutableList> = mutableListOf() - - /** - * List of registered slash commands. - * - * Unlike normal commands, slash commands cannot be unregistered dynamically. However, slash commands that - * belong to unloaded extensions will not execute. - */ - public open val messageCommands: MutableList> = mutableListOf() - - /** - * List of registered slash commands. - * - * Unlike normal commands, slash commands cannot be unregistered dynamically. However, slash commands that - * belong to unloaded extensions will not execute. - */ - public open val slashCommands: MutableList> = mutableListOf() - - /** - * List of registered slash commands. - * - * Unlike normal commands, slash commands cannot be unregistered dynamically. However, slash commands that - * belong to unloaded extensions will not execute. - */ - public open val userCommands: MutableList> = mutableListOf() - - /** - * List of chat command checks. - * - * These checks will be checked against all chat commands in this extension. - */ - public open val chatCommandChecks: MutableList = - mutableListOf() - - /** - * Whether [ApplicationCommands][ApplicationCommand] should be allowed in DMs by default. - * - * @see ApplicationCommand.allowInDms - */ - public open val allowApplicationCommandInDMs: Boolean = true - - /** - * List of message command checks. - * - * These checks will be checked against all message commands in this extension. - */ - public val messageCommandChecks: MutableList = mutableListOf() - - /** - * List of slash command checks. - * - * These checks will be checked against all slash commands in this extension. - */ - public val slashCommandChecks: MutableList = mutableListOf() - - /** - * List of user command checks. - * - * These checks will be checked against all user commands in this extension. - */ - public val userCommandChecks: MutableList = mutableListOf() - - /** String representing the bundle to get translations from for command names/descriptions. **/ + /** The [ExtensibleBot] instance that this extension is installed to. **/ + public open val bot: ExtensibleBot by inject() + + /** Current Kord instance powering the bot. **/ + public open val kord: Kord by inject() + + /** Message command registry. **/ + public open val chatCommandRegistry: ChatCommandRegistry by inject() + + /** Slash command registry. **/ + public open val applicationCommandRegistry: ApplicationCommandRegistry by inject() + + /** + * The name of this extension. + * + * Ensure you override this in your extension. This should be a unique name that can later + * be used to refer to your specific extension after it's been registered. + */ + public abstract val name: String + + /** + * The current loading/unloading state of the extension. + */ + public open var state: ExtensionState = ExtensionState.UNLOADED + + /** Check whether this extension's state is [ExtensionState.LOADED]. **/ + public open val loaded: Boolean get() = state == ExtensionState.LOADED + + /** + * List of registered event handlers. + * + * When an extension is unloaded, all the event handlers are cancelled and + * removed from the bot. + */ + public open val eventHandlers: MutableList> = mutableListOf() + + /** + * List of registered commands. + * + * When an extension is unloaded, all the commands are removed from the bot. + */ + public open val chatCommands: MutableList> = mutableListOf() + + /** + * List of registered slash commands. + * + * Unlike normal commands, slash commands cannot be unregistered dynamically. However, slash commands that + * belong to unloaded extensions will not execute. + */ + public open val messageCommands: MutableList> = mutableListOf() + + /** + * List of registered slash commands. + * + * Unlike normal commands, slash commands cannot be unregistered dynamically. However, slash commands that + * belong to unloaded extensions will not execute. + */ + public open val slashCommands: MutableList> = mutableListOf() + + /** + * List of registered slash commands. + * + * Unlike normal commands, slash commands cannot be unregistered dynamically. However, slash commands that + * belong to unloaded extensions will not execute. + */ + public open val userCommands: MutableList> = mutableListOf() + + /** + * List of chat command checks. + * + * These checks will be checked against all chat commands in this extension. + */ + public open val chatCommandChecks: MutableList = + mutableListOf() + + /** + * Whether [ApplicationCommands][ApplicationCommand] should be allowed in DMs by default. + * + * @see ApplicationCommand.allowInDms + */ + public open val allowApplicationCommandInDMs: Boolean = true + + /** + * List of message command checks. + * + * These checks will be checked against all message commands in this extension. + */ + public val messageCommandChecks: MutableList = mutableListOf() + + /** + * List of slash command checks. + * + * These checks will be checked against all slash commands in this extension. + */ + public val slashCommandChecks: MutableList = mutableListOf() + + /** + * List of user command checks. + * + * These checks will be checked against all user commands in this extension. + */ + public val userCommandChecks: MutableList = mutableListOf() + + /** String representing the bundle to get translations from for command names/descriptions. **/ @Translatable(TranslatableType.BUNDLE) public open val bundle: String? = null - /** Set of intents required by this extension's event handlers and commands. **/ - public open val intents: MutableSet = mutableSetOf() - - /** - * Override this in your subclass and use it to register your commands and event - * handlers. - * - * This function simply allows you to register commands and event handlers in the context - * of a suspended function, which is required in order to make use of some other APIs. As a - * result, we recommend you make use of this in all your extensions, instead of init {} - * blocks. - * - * This function is called on first extension load, and whenever it's reloaded after that. - */ - public abstract suspend fun setup() - - /** - * @suppress This is an internal API function used as part of extension lifecycle management. - */ - public open suspend fun doSetup() { - this.setState(ExtensionState.LOADING) - - @Suppress("TooGenericExceptionCaught") - try { - this.setup() - } catch (t: Throwable) { - this.setState(ExtensionState.FAILED_LOADING) - throw t - } - - this.setState(ExtensionState.LOADED) - } - - /** Update this extension's state, firing the extension state change event. **/ - public open suspend fun setState(state: ExtensionState) { - bot.send(ExtensionStateEvent(this, state)) - - this.state = state - } - - /** - * If you need to, override this function and use it to clean up your extension when - * it's unloaded. - * - * You do not need to override this to clean up commands and event handlers, that's - * handled for you. - */ - public open suspend fun unload() { - logger.trace { "Unload function not overridden." } - } - - /** - * Unload all event handlers and commands for this extension. - * - * This function is called as part of unloading extensions, which may be - * done programmatically. - * - * @suppress Internal function - */ - public open suspend fun doUnload() { - var error: Throwable? = null - - this.setState(ExtensionState.UNLOADING) - - @Suppress("TooGenericExceptionCaught") - try { - this.unload() - } catch (t: Throwable) { - error = t - - this.setState(ExtensionState.FAILED_UNLOADING) - } - - for (handler in eventHandlers) { - handler.job?.cancel() - bot.removeEventHandler(handler) - } - - for (command in chatCommands) { - chatCommandRegistry.remove(command) - } - - eventHandlers.clear() - chatCommands.clear() - - if (error != null) { - throw error - } - - this.setState(ExtensionState.UNLOADED) - } + /** Set of intents required by this extension's event handlers and commands. **/ + public open val intents: MutableSet = mutableSetOf() + + /** + * Override this in your subclass and use it to register your commands and event + * handlers. + * + * This function simply allows you to register commands and event handlers in the context + * of a suspended function, which is required in order to make use of some other APIs. As a + * result, we recommend you make use of this in all your extensions, instead of init {} + * blocks. + * + * This function is called on first extension load, and whenever it's reloaded after that. + */ + public abstract suspend fun setup() + + /** + * @suppress This is an internal API function used as part of extension lifecycle management. + */ + public open suspend fun doSetup() { + this.setState(ExtensionState.LOADING) + + @Suppress("TooGenericExceptionCaught") + try { + this.setup() + } catch (t: Throwable) { + this.setState(ExtensionState.FAILED_LOADING) + throw t + } + + this.setState(ExtensionState.LOADED) + } + + /** Update this extension's state, firing the extension state change event. **/ + public open suspend fun setState(state: ExtensionState) { + bot.send(ExtensionStateEvent(this, state)) + + this.state = state + } + + /** + * If you need to, override this function and use it to clean up your extension when + * it's unloaded. + * + * You do not need to override this to clean up commands and event handlers, that's + * handled for you. + */ + public open suspend fun unload() { + logger.trace { "Unload function not overridden." } + } + + /** + * Unload all event handlers and commands for this extension. + * + * This function is called as part of unloading extensions, which may be + * done programmatically. + * + * @suppress Internal function + */ + public open suspend fun doUnload() { + var error: Throwable? = null + + this.setState(ExtensionState.UNLOADING) + + @Suppress("TooGenericExceptionCaught") + try { + this.unload() + } catch (t: Throwable) { + error = t + + this.setState(ExtensionState.FAILED_UNLOADING) + } + + for (handler in eventHandlers) { + handler.job?.cancel() + bot.removeEventHandler(handler) + } + + for (command in chatCommands) { + chatCommandRegistry.remove(command) + } + + eventHandlers.clear() + chatCommands.clear() + + if (error != null) { + throw error + } + + this.setState(ExtensionState.UNLOADED) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/ExtensionState.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/ExtensionState.kt index acf647bfe9..895aa3ab8a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/ExtensionState.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/ExtensionState.kt @@ -8,12 +8,12 @@ package com.kotlindiscord.kord.extensions.extensions /** Extension states, which describe what state of loading/unloading an extension is currently in. **/ public enum class ExtensionState { - FAILED_LOADING, - FAILED_UNLOADING, + FAILED_LOADING, + FAILED_UNLOADING, - LOADED, - LOADING, + LOADED, + LOADING, - UNLOADED, - UNLOADING, + UNLOADED, + UNLOADING, } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/_Commands.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/_Commands.kt index d978fa8a42..d4fbfd2552 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/_Commands.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/_Commands.kt @@ -45,7 +45,7 @@ private val logger = KotlinLogging.logger {} * @param checks Checks to apply to all slash commands. */ public fun Extension.messageCommandCheck(vararg checks: MessageCommandCheck) { - checks.forEach { messageCommandChecks.add(it) } + checks.forEach { messageCommandChecks.add(it) } } /** @@ -54,95 +54,95 @@ public fun Extension.messageCommandCheck(vararg checks: MessageCommandCheck) { * @param check Check to apply to all slash commands. */ public fun Extension.messageCommandCheck(check: MessageCommandCheck) { - messageCommandChecks.add(check) + messageCommandChecks.add(check) } /** Register an ephemeral message command, DSL-style. **/ @ExtensionDSL public suspend fun Extension.ephemeralMessageCommand( - body: suspend EphemeralMessageCommand.() -> Unit + body: suspend EphemeralMessageCommand.() -> Unit, ): EphemeralMessageCommand { - val commandObj = EphemeralMessageCommand(this) - body(commandObj) + val commandObj = EphemeralMessageCommand(this) + body(commandObj) - return ephemeralMessageCommand(commandObj) + return ephemeralMessageCommand(commandObj) } /** Register an ephemeral message command, DSL-style. **/ @ExtensionDSL -public suspend fun Extension.ephemeralMessageCommand( - modal: (() -> M), - body: suspend EphemeralMessageCommand.() -> Unit +public suspend fun Extension.ephemeralMessageCommand( + modal: (() -> M), + body: suspend EphemeralMessageCommand.() -> Unit, ): EphemeralMessageCommand { - val commandObj = EphemeralMessageCommand(this, modal) - body(commandObj) + val commandObj = EphemeralMessageCommand(this, modal) + body(commandObj) - return ephemeralMessageCommand(commandObj) + return ephemeralMessageCommand(commandObj) } /** Register a custom instance of an ephemeral message command. **/ @ExtensionDSL public suspend fun Extension.ephemeralMessageCommand( - commandObj: EphemeralMessageCommand + commandObj: EphemeralMessageCommand, ): EphemeralMessageCommand { - try { - commandObj.validate() - messageCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } + try { + commandObj.validate() + messageCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } - return commandObj + return commandObj } /** Register a public message command, DSL-style. **/ @ExtensionDSL public suspend fun Extension.publicMessageCommand( - body: suspend PublicMessageCommand.() -> Unit + body: suspend PublicMessageCommand.() -> Unit, ): PublicMessageCommand { - val commandObj = PublicMessageCommand(this) - body(commandObj) + val commandObj = PublicMessageCommand(this) + body(commandObj) - return publicMessageCommand(commandObj) + return publicMessageCommand(commandObj) } /** Register a public message command, DSL-style. **/ @ExtensionDSL public suspend fun Extension.publicMessageCommand( - modal: (() -> M), - body: suspend PublicMessageCommand.() -> Unit + modal: (() -> M), + body: suspend PublicMessageCommand.() -> Unit, ): PublicMessageCommand { - val commandObj = PublicMessageCommand(this, modal) - body(commandObj) + val commandObj = PublicMessageCommand(this, modal) + body(commandObj) - return publicMessageCommand(commandObj) + return publicMessageCommand(commandObj) } /** Register a custom instance of a public message command. **/ @ExtensionDSL public suspend fun Extension.publicMessageCommand( - commandObj: PublicMessageCommand + commandObj: PublicMessageCommand, ): PublicMessageCommand { - try { - commandObj.validate() - messageCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } + try { + commandObj.validate() + messageCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } - return commandObj + return commandObj } // endregion @@ -162,7 +162,7 @@ public suspend fun Extension.publicMessageCommand( * @param checks Checks to apply to all slash commands. */ public fun Extension.slashCommandCheck(vararg checks: SlashCommandCheck) { - checks.forEach { slashCommandChecks.add(it) } + checks.forEach { slashCommandChecks.add(it) } } /** @@ -171,7 +171,7 @@ public fun Extension.slashCommandCheck(vararg checks: SlashCommandCheck) { * @param check Check to apply to all slash commands. */ public fun Extension.slashCommandCheck(check: SlashCommandCheck) { - slashCommandChecks.add(check) + slashCommandChecks.add(check) } // endregion @@ -188,13 +188,13 @@ public fun Extension.slashCommandCheck(check: SlashCommandCheck) { */ @ExtensionDSL public suspend fun Extension.ephemeralSlashCommand( - arguments: () -> T, - body: suspend EphemeralSlashCommand.() -> Unit + arguments: () -> T, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand(this, arguments, null, null) - body(commandObj) + val commandObj = EphemeralSlashCommand(this, arguments, null, null) + body(commandObj) - return ephemeralSlashCommand(commandObj) + return ephemeralSlashCommand(commandObj) } /** @@ -208,20 +208,20 @@ public suspend fun Extension.ephemeralSlashCommand( @ExtensionDSL @JvmName("ephemeralSlashCommand1") public suspend fun Extension.ephemeralSlashCommand( - modal: () -> M, - body: suspend EphemeralSlashCommand.() -> Unit + modal: () -> M, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - this, - null, - modal, - null, - null - ) + val commandObj = EphemeralSlashCommand( + this, + null, + modal, + null, + null + ) - body(commandObj) + body(commandObj) - return ephemeralSlashCommand(commandObj) + return ephemeralSlashCommand(commandObj) } /** @@ -235,21 +235,21 @@ public suspend fun Extension.ephemeralSlashCommand( */ @ExtensionDSL public suspend fun Extension.ephemeralSlashCommand( - arguments: () -> A, - modal: () -> M, - body: suspend EphemeralSlashCommand.() -> Unit + arguments: () -> A, + modal: () -> M, + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - this, - arguments, - modal, - null, - null - ) + val commandObj = EphemeralSlashCommand( + this, + arguments, + modal, + null, + null + ) - body(commandObj) + body(commandObj) - return ephemeralSlashCommand(commandObj) + return ephemeralSlashCommand(commandObj) } /** @@ -261,22 +261,22 @@ public suspend fun Extension.ephemeralSlashComman */ @ExtensionDSL public suspend fun Extension.ephemeralSlashCommand( - commandObj: EphemeralSlashCommand + commandObj: EphemeralSlashCommand, ): EphemeralSlashCommand { - try { - commandObj.validate() - slashCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register subcommand - $e" } - } + try { + commandObj.validate() + slashCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register subcommand - $e" } + } - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } - return commandObj + return commandObj } /** @@ -288,19 +288,19 @@ public suspend fun Extension.ephemeralSlashComman */ @ExtensionDSL public suspend fun Extension.ephemeralSlashCommand( - body: suspend EphemeralSlashCommand.() -> Unit + body: suspend EphemeralSlashCommand.() -> Unit, ): EphemeralSlashCommand { - val commandObj = EphemeralSlashCommand( - this, - null, - null, - null, - null - ) + val commandObj = EphemeralSlashCommand( + this, + null, + null, + null, + null + ) - body(commandObj) + body(commandObj) - return ephemeralSlashCommand(commandObj) + return ephemeralSlashCommand(commandObj) } // endregion @@ -317,20 +317,20 @@ public suspend fun Extension.ephemeralSlashCommand( */ @ExtensionDSL public suspend fun Extension.publicSlashCommand( - arguments: () -> T, - body: suspend PublicSlashCommand.() -> Unit + arguments: () -> T, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - this, - arguments, - null, - null, - null - ) + val commandObj = PublicSlashCommand( + this, + arguments, + null, + null, + null + ) - body(commandObj) + body(commandObj) - return publicSlashCommand(commandObj) + return publicSlashCommand(commandObj) } /** @@ -344,20 +344,20 @@ public suspend fun Extension.publicSlashCommand( @ExtensionDSL @JvmName("publicSlashCommand1") public suspend fun Extension.publicSlashCommand( - modal: () -> M, - body: suspend PublicSlashCommand.() -> Unit + modal: () -> M, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - this, - null, - modal, - null, - null - ) + val commandObj = PublicSlashCommand( + this, + null, + modal, + null, + null + ) - body(commandObj) + body(commandObj) - return publicSlashCommand(commandObj) + return publicSlashCommand(commandObj) } /** @@ -371,20 +371,20 @@ public suspend fun Extension.publicSlashCommand( */ @ExtensionDSL public suspend fun Extension.publicSlashCommand( - arguments: () -> A, - modal: () -> M, - body: suspend PublicSlashCommand.() -> Unit + arguments: () -> A, + modal: () -> M, + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - this, - arguments, - modal, - null - ) + val commandObj = PublicSlashCommand( + this, + arguments, + modal, + null + ) - body(commandObj) + body(commandObj) - return publicSlashCommand(commandObj) + return publicSlashCommand(commandObj) } /** @@ -396,22 +396,22 @@ public suspend fun Extension.publicSlashCommand( */ @ExtensionDSL public suspend fun Extension.publicSlashCommand( - commandObj: PublicSlashCommand + commandObj: PublicSlashCommand, ): PublicSlashCommand { - try { - commandObj.validate() - slashCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register subcommand - $e" } - } + try { + commandObj.validate() + slashCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register subcommand - $e" } + } - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } - return commandObj + return commandObj } /** @@ -423,19 +423,19 @@ public suspend fun Extension.publicSlashCommand( */ @ExtensionDSL public suspend fun Extension.publicSlashCommand( - body: suspend PublicSlashCommand.() -> Unit + body: suspend PublicSlashCommand.() -> Unit, ): PublicSlashCommand { - val commandObj = PublicSlashCommand( - this, - null, - null, - null, - null - ) + val commandObj = PublicSlashCommand( + this, + null, + null, + null, + null + ) - body(commandObj) + body(commandObj) - return publicSlashCommand(commandObj) + return publicSlashCommand(commandObj) } // endregion @@ -455,7 +455,7 @@ public suspend fun Extension.publicSlashCommand( * @param checks Checks to apply to all slash commands. */ public fun Extension.userCommandCheck(vararg checks: UserCommandCheck) { - checks.forEach { userCommandChecks.add(it) } + checks.forEach { userCommandChecks.add(it) } } /** @@ -464,95 +464,95 @@ public fun Extension.userCommandCheck(vararg checks: UserCommandCheck) { * @param check Check to apply to all slash commands. */ public fun Extension.userCommandCheck(check: UserCommandCheck) { - userCommandChecks.add(check) + userCommandChecks.add(check) } /** Register an ephemeral user command, DSL-style. **/ @ExtensionDSL public suspend fun Extension.ephemeralUserCommand( - body: suspend EphemeralUserCommand.() -> Unit + body: suspend EphemeralUserCommand.() -> Unit, ): EphemeralUserCommand { - val commandObj = EphemeralUserCommand(this) - body(commandObj) + val commandObj = EphemeralUserCommand(this) + body(commandObj) - return ephemeralUserCommand(commandObj) + return ephemeralUserCommand(commandObj) } /** Register an ephemeral user command, DSL-style. **/ @ExtensionDSL public suspend fun Extension.ephemeralUserCommand( - modal: (() -> M), - body: suspend EphemeralUserCommand.() -> Unit + modal: (() -> M), + body: suspend EphemeralUserCommand.() -> Unit, ): EphemeralUserCommand { - val commandObj = EphemeralUserCommand(this, modal) - body(commandObj) + val commandObj = EphemeralUserCommand(this, modal) + body(commandObj) - return ephemeralUserCommand(commandObj) + return ephemeralUserCommand(commandObj) } /** Register a custom instance of an ephemeral user command. **/ @ExtensionDSL public suspend fun Extension.ephemeralUserCommand( - commandObj: EphemeralUserCommand + commandObj: EphemeralUserCommand, ): EphemeralUserCommand { - try { - commandObj.validate() - userCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } + try { + commandObj.validate() + userCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } - return commandObj + return commandObj } /** Register a public user command, DSL-style. **/ @ExtensionDSL public suspend fun Extension.publicUserCommand( - body: suspend PublicUserCommand.() -> Unit + body: suspend PublicUserCommand.() -> Unit, ): PublicUserCommand { - val commandObj = PublicUserCommand(this) - body(commandObj) + val commandObj = PublicUserCommand(this) + body(commandObj) - return publicUserCommand(commandObj) + return publicUserCommand(commandObj) } /** Register a public user command, DSL-style. **/ @ExtensionDSL public suspend fun Extension.publicUserCommand( - modal: (() -> M), - body: suspend PublicUserCommand.() -> Unit + modal: (() -> M), + body: suspend PublicUserCommand.() -> Unit, ): PublicUserCommand { - val commandObj = PublicUserCommand(this, modal) - body(commandObj) + val commandObj = PublicUserCommand(this, modal) + body(commandObj) - return publicUserCommand(commandObj) + return publicUserCommand(commandObj) } /** Register a custom instance of a public user command. **/ @ExtensionDSL public suspend fun Extension.publicUserCommand( - commandObj: PublicUserCommand + commandObj: PublicUserCommand, ): PublicUserCommand { - try { - commandObj.validate() - userCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } + try { + commandObj.validate() + userCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } - return commandObj + return commandObj } // endregion @@ -573,7 +573,7 @@ public suspend fun Extension.publicUserCommand( */ @ExtensionDSL public fun Extension.chatCommandCheck(vararg checks: ChatCommandCheck) { - checks.forEach { chatCommandChecks.add(it) } + checks.forEach { chatCommandChecks.add(it) } } /** @@ -583,7 +583,7 @@ public fun Extension.chatCommandCheck(vararg checks: ChatCommandCheck) { */ @ExtensionDSL public fun Extension.chatCommandCheck(check: ChatCommandCheck) { - chatCommandChecks.add(check) + chatCommandChecks.add(check) } /** @@ -595,13 +595,13 @@ public fun Extension.chatCommandCheck(check: ChatCommandCheck) { */ @ExtensionDSL public suspend fun Extension.chatCommand( - arguments: () -> T, - body: suspend ChatCommand.() -> Unit + arguments: () -> T, + body: suspend ChatCommand.() -> Unit, ): ChatCommand { - val commandObj = ChatCommand(this, arguments) - body.invoke(commandObj) + val commandObj = ChatCommand(this, arguments) + body.invoke(commandObj) - return chatCommand(commandObj) + return chatCommand(commandObj) } /** @@ -613,12 +613,12 @@ public suspend fun Extension.chatCommand( */ @ExtensionDSL public suspend fun Extension.chatCommand( - body: suspend ChatCommand.() -> Unit + body: suspend ChatCommand.() -> Unit, ): ChatCommand { - val commandObj = ChatCommand(this) - body.invoke(commandObj) + val commandObj = ChatCommand(this) + body.invoke(commandObj) - return chatCommand(commandObj) + return chatCommand(commandObj) } /** @@ -630,24 +630,24 @@ public suspend fun Extension.chatCommand( */ @ExtensionDSL public fun Extension.chatCommand( - commandObj: ChatCommand + commandObj: ChatCommand, ): ChatCommand { - try { - commandObj.validate() - chatCommandRegistry.add(commandObj) - chatCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register command - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register command - $e" } - } + try { + commandObj.validate() + chatCommandRegistry.add(commandObj) + chatCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register command - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register command - $e" } + } - if (chatCommandRegistry.enabled) { // Don't add the intents if they won't be used - intents += Intent.DirectMessages - intents += Intent.GuildMessages - } + if (chatCommandRegistry.enabled) { // Don't add the intents if they won't be used + intents += Intent.DirectMessages + intents += Intent.GuildMessages + } - return commandObj + return commandObj } /** @@ -662,13 +662,13 @@ public fun Extension.chatCommand( */ @ExtensionDSL public suspend fun Extension.chatGroupCommand( - arguments: () -> T, - body: suspend ChatGroupCommand.() -> Unit + arguments: () -> T, + body: suspend ChatGroupCommand.() -> Unit, ): ChatGroupCommand { - val commandObj = ChatGroupCommand(this, arguments) - body.invoke(commandObj) + val commandObj = ChatGroupCommand(this, arguments) + body.invoke(commandObj) - return chatCommand(commandObj) as ChatGroupCommand + return chatCommand(commandObj) as ChatGroupCommand } /** @@ -683,12 +683,12 @@ public suspend fun Extension.chatGroupCommand( */ @ExtensionDSL public suspend fun Extension.chatGroupCommand( - body: suspend ChatGroupCommand.() -> Unit + body: suspend ChatGroupCommand.() -> Unit, ): ChatGroupCommand { - val commandObj = ChatGroupCommand(this) - body.invoke(commandObj) + val commandObj = ChatGroupCommand(this) + body.invoke(commandObj) - return chatCommand(commandObj) as ChatGroupCommand + return chatCommand(commandObj) as ChatGroupCommand } // endregion diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/_Events.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/_Events.kt index 2d5126a6b8..d9eb91b55a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/_Events.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/_Events.kt @@ -23,32 +23,32 @@ import io.github.oshai.kotlinlogging.KotlinLogging */ public suspend inline fun Extension.event( noinline constructor: (Extension) -> EventHandler = ::EventHandler, - noinline body: suspend EventHandler.() -> Unit + noinline body: suspend EventHandler.() -> Unit, ): EventHandler { - val eventHandler = constructor(this) - val logger = KotlinLogging.logger {} + val eventHandler = constructor(this) + val logger = KotlinLogging.logger {} - body.invoke(eventHandler) + body.invoke(eventHandler) - try { - eventHandler.validate() + try { + eventHandler.validate() - eventHandler.listenerRegistrationCallable = { - eventHandler.job = bot.registerListenerForHandler(eventHandler) - } + eventHandler.listenerRegistrationCallable = { + eventHandler.job = bot.registerListenerForHandler(eventHandler) + } - bot.addEventHandler(eventHandler) - eventHandlers.add(eventHandler) - } catch (e: EventHandlerRegistrationException) { - logger.error(e) { "Failed to register event handler - $e" } - } catch (e: InvalidEventHandlerException) { - logger.error(e) { "Failed to register event handler - $e" } - } + bot.addEventHandler(eventHandler) + eventHandlers.add(eventHandler) + } catch (e: EventHandlerRegistrationException) { + logger.error(e) { "Failed to register event handler - $e" } + } catch (e: InvalidEventHandlerException) { + logger.error(e) { "Failed to register event handler - $e" } + } - val fakeBuilder = Intents.Builder() + val fakeBuilder = Intents.Builder() - fakeBuilder.enableEvent() - intents += fakeBuilder.build().values + fakeBuilder.enableEvent() + intents += fakeBuilder.build().values - return eventHandler + return eventHandler } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/base/HelpProvider.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/base/HelpProvider.kt index ba7af1783d..7a83aec750 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/base/HelpProvider.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/base/HelpProvider.kt @@ -24,174 +24,174 @@ import dev.kord.core.event.message.MessageCreateEvent * before writing your own implementation. */ public interface HelpProvider { - /** - * Given a command object and command prefix string, return a triple representing the formatted command name and - * signature, formatted command description and formatted argument list. - * - * @param prefix Command prefix character to use while formatting. - * @param event MessageCreateEvent that triggered this help invocation, used to run subcommand checks. - * @param command Command object to format the help for. - * @param longDescription Whether to include more than the first line of the command description, `false` by - * default. - * - * @return Tripe containing three formatted elements - the command's name and signature with prefix, the command's - * description, and the command's argument list. - */ - public suspend fun formatCommandHelp( - prefix: String, - event: MessageCreateEvent, - command: ChatCommand, - longDescription: Boolean = false - ): Triple - - /** - * Given a command object and command context, return a triple representing the formatted command name and - * signature, formatted command description and formatted argument list. - * - * @param context MessageCommandContext object that triggered this help invocation. - * @param command Command object to format the help for. - * @param longDescription Whether to include more than the first line of the command description, `false` by - * default. - * - * @return Tripe containing three formatted elements - the command's name and signature with prefix, the command's - * description, and the command's argument list. - */ - public suspend fun formatCommandHelp( - context: ChatCommandContext<*>, - command: ChatCommand, - longDescription: Boolean = false - ): Triple { - val prefix = getKoin().get().getPrefix(context.event) - - return formatCommandHelp(prefix, context.event, command, longDescription) - } - - /** - * Gather all available commands (with passing checks) from the bot, and return them. - */ - public suspend fun gatherCommands(event: MessageCreateEvent): List> - - /** - * Return the [MessageCommand] specified in the arguments, or `null` if it can't be found (or the checks fail). - */ - public suspend fun getCommand(event: MessageCreateEvent, args: List): ChatCommand? - - /** - * Given an event, prefix and argument list, attempt to find the command represented by the arguments and return - * a [BasePaginator], ready to be sent. - * - * The [BasePaginator] will contain an error message if the command can't be found, or the command's checks fail. - * - * @param event MessageCreateEvent that triggered this help invocation. - * @param prefix Command prefix to use for formatting. - * @param args List of arguments to use to find the command. - * - * @return Paginator containing the command's help, or an error message. - */ - public suspend fun getCommandHelpPaginator( - event: MessageCreateEvent, - prefix: String, - args: List - ): BasePaginator - - /** - * Given a command context and argument list, attempt to find the command represented by the arguments and - * return a [BasePaginator], ready to be sent. - * - * The [BasePaginator] will contain an error message if the command can't be found, or the command's checks fail. - * - * @param context MessageCommandContext object that triggered this help invocation. - * @param args List of arguments to use to find the command. - * - * @return Paginator containing the command's help, or an error message. - */ - public suspend fun getCommandHelpPaginator( - context: ChatCommandContext<*>, - args: List - ): BasePaginator { - val prefix = getKoin().get().getPrefix(context.event) - - return getCommandHelpPaginator(context.event, prefix, args) - } - - /** - * Given an event, prefix and argument list, attempt to find the command represented by the arguments and return - * a [BasePaginator], ready to be sent. - * - * The [BasePaginator] will contain an error message if the command passed was `null`, or the command's checks fail. - * - * Please be mindful of using this with subcommands, as the extension's design intends for users to be unable to - * retrieve help for subcommands when any parent command's checks fail, and this function does not run those checks. - * - * @param event MessageCreateEvent that triggered this help invocation. - * @param prefix Command prefix to use for formatting. - * @param command Command object to format the help for. - * - * @return Paginator containing the command's help, or an error message. - */ - public suspend fun getCommandHelpPaginator( - event: MessageCreateEvent, - prefix: String, - command: ChatCommand? - ): BasePaginator - - /** - * Given a command context and argument list, attempt to find the command represented by the arguments and return - * a [BasePaginator], ready to be sent. - * - * The [BasePaginator] will contain an error message if the command passed was `null`, or the command's checks fail. - * - * Please be mindful of using this with subcommands, as the extension's design intends for users to be unable to - * retrieve help for subcommands when any parent command's checks fail, and this function does not run those checks. - * - * @param context MessageCommandContext object that triggered this help invocation. - * @param command Command object to format the help for. - * - * @return Paginator containing the command's help, or an error message. - */ - public suspend fun getCommandHelpPaginator( - context: ChatCommandContext<*>, - command: ChatCommand? - ): BasePaginator { - val prefix = getKoin().get().getPrefix(context.event) - - return getCommandHelpPaginator(context.event, prefix, command) - } - - /** - * Given an event and prefix, return a [BasePaginator] containing help information for all loaded commands with - * passing checks. - * - * While it shouldn't really be possible, this will also handle the case where there are no commands registered - * at all, for all you weirdos out there breaking everything intentionally. - * - * This will only return help information for commands with checks that pass. If a command's checks fail, it will - * not be listed. Similarly, a command will only show subcommands with passing checks. - * - * @param event MessageCreateEvent that triggered this help invocation. - * @param prefix Command prefix to use for formatting. - * - * @return Paginator containing help information for all loaded commands with passing checks. - */ - public suspend fun getMainHelpPaginator(event: MessageCreateEvent, prefix: String): BasePaginator - - /** - * Given a command context, return a [BasePaginator] containing help information for all loaded commands with - * passing checks. - * - * While it shouldn't really be possible, this will also handle the case where there are no commands registered - * at all, for all you weirdos out there breaking everything intentionally. - * - * This will only return help information for commands with checks that pass. If a command's checks fail, it will - * not be listed. Similarly, a command will only show subcommands with passing checks. - * - * @param context MessageCommandContext object that triggered this help invocation.. - * - * @return BasePaginator containing help information for all loaded commands with passing checks. - */ - public suspend fun getMainHelpPaginator(context: ChatCommandContext<*>): BasePaginator { - val prefix = getKoin().get().getPrefix(context.event) - - return getMainHelpPaginator(context.event, prefix) - } + /** + * Given a command object and command prefix string, return a triple representing the formatted command name and + * signature, formatted command description and formatted argument list. + * + * @param prefix Command prefix character to use while formatting. + * @param event MessageCreateEvent that triggered this help invocation, used to run subcommand checks. + * @param command Command object to format the help for. + * @param longDescription Whether to include more than the first line of the command description, `false` by + * default. + * + * @return Tripe containing three formatted elements - the command's name and signature with prefix, the command's + * description, and the command's argument list. + */ + public suspend fun formatCommandHelp( + prefix: String, + event: MessageCreateEvent, + command: ChatCommand, + longDescription: Boolean = false, + ): Triple + + /** + * Given a command object and command context, return a triple representing the formatted command name and + * signature, formatted command description and formatted argument list. + * + * @param context MessageCommandContext object that triggered this help invocation. + * @param command Command object to format the help for. + * @param longDescription Whether to include more than the first line of the command description, `false` by + * default. + * + * @return Tripe containing three formatted elements - the command's name and signature with prefix, the command's + * description, and the command's argument list. + */ + public suspend fun formatCommandHelp( + context: ChatCommandContext<*>, + command: ChatCommand, + longDescription: Boolean = false, + ): Triple { + val prefix = getKoin().get().getPrefix(context.event) + + return formatCommandHelp(prefix, context.event, command, longDescription) + } + + /** + * Gather all available commands (with passing checks) from the bot, and return them. + */ + public suspend fun gatherCommands(event: MessageCreateEvent): List> + + /** + * Return the [MessageCommand] specified in the arguments, or `null` if it can't be found (or the checks fail). + */ + public suspend fun getCommand(event: MessageCreateEvent, args: List): ChatCommand? + + /** + * Given an event, prefix and argument list, attempt to find the command represented by the arguments and return + * a [BasePaginator], ready to be sent. + * + * The [BasePaginator] will contain an error message if the command can't be found, or the command's checks fail. + * + * @param event MessageCreateEvent that triggered this help invocation. + * @param prefix Command prefix to use for formatting. + * @param args List of arguments to use to find the command. + * + * @return Paginator containing the command's help, or an error message. + */ + public suspend fun getCommandHelpPaginator( + event: MessageCreateEvent, + prefix: String, + args: List, + ): BasePaginator + + /** + * Given a command context and argument list, attempt to find the command represented by the arguments and + * return a [BasePaginator], ready to be sent. + * + * The [BasePaginator] will contain an error message if the command can't be found, or the command's checks fail. + * + * @param context MessageCommandContext object that triggered this help invocation. + * @param args List of arguments to use to find the command. + * + * @return Paginator containing the command's help, or an error message. + */ + public suspend fun getCommandHelpPaginator( + context: ChatCommandContext<*>, + args: List, + ): BasePaginator { + val prefix = getKoin().get().getPrefix(context.event) + + return getCommandHelpPaginator(context.event, prefix, args) + } + + /** + * Given an event, prefix and argument list, attempt to find the command represented by the arguments and return + * a [BasePaginator], ready to be sent. + * + * The [BasePaginator] will contain an error message if the command passed was `null`, or the command's checks fail. + * + * Please be mindful of using this with subcommands, as the extension's design intends for users to be unable to + * retrieve help for subcommands when any parent command's checks fail, and this function does not run those checks. + * + * @param event MessageCreateEvent that triggered this help invocation. + * @param prefix Command prefix to use for formatting. + * @param command Command object to format the help for. + * + * @return Paginator containing the command's help, or an error message. + */ + public suspend fun getCommandHelpPaginator( + event: MessageCreateEvent, + prefix: String, + command: ChatCommand?, + ): BasePaginator + + /** + * Given a command context and argument list, attempt to find the command represented by the arguments and return + * a [BasePaginator], ready to be sent. + * + * The [BasePaginator] will contain an error message if the command passed was `null`, or the command's checks fail. + * + * Please be mindful of using this with subcommands, as the extension's design intends for users to be unable to + * retrieve help for subcommands when any parent command's checks fail, and this function does not run those checks. + * + * @param context MessageCommandContext object that triggered this help invocation. + * @param command Command object to format the help for. + * + * @return Paginator containing the command's help, or an error message. + */ + public suspend fun getCommandHelpPaginator( + context: ChatCommandContext<*>, + command: ChatCommand?, + ): BasePaginator { + val prefix = getKoin().get().getPrefix(context.event) + + return getCommandHelpPaginator(context.event, prefix, command) + } + + /** + * Given an event and prefix, return a [BasePaginator] containing help information for all loaded commands with + * passing checks. + * + * While it shouldn't really be possible, this will also handle the case where there are no commands registered + * at all, for all you weirdos out there breaking everything intentionally. + * + * This will only return help information for commands with checks that pass. If a command's checks fail, it will + * not be listed. Similarly, a command will only show subcommands with passing checks. + * + * @param event MessageCreateEvent that triggered this help invocation. + * @param prefix Command prefix to use for formatting. + * + * @return Paginator containing help information for all loaded commands with passing checks. + */ + public suspend fun getMainHelpPaginator(event: MessageCreateEvent, prefix: String): BasePaginator + + /** + * Given a command context, return a [BasePaginator] containing help information for all loaded commands with + * passing checks. + * + * While it shouldn't really be possible, this will also handle the case where there are no commands registered + * at all, for all you weirdos out there breaking everything intentionally. + * + * This will only return help information for commands with checks that pass. If a command's checks fail, it will + * not be listed. Similarly, a command will only show subcommands with passing checks. + * + * @param context MessageCommandContext object that triggered this help invocation.. + * + * @return BasePaginator containing help information for all loaded commands with passing checks. + */ + public suspend fun getMainHelpPaginator(context: ChatCommandContext<*>): BasePaginator { + val prefix = getKoin().get().getPrefix(context.event) + + return getMainHelpPaginator(context.event, prefix) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/impl/HelpExtension.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/impl/HelpExtension.kt index 5e24f24965..85ac76b5dd 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/impl/HelpExtension.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/impl/HelpExtension.kt @@ -41,413 +41,413 @@ private const val ARGUMENTS_GROUP = "Arguments" */ @Suppress("StringLiteralDuplication") public class HelpExtension : HelpProvider, Extension() { - override val name: String = "help" - - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by inject() - - /** Message command registry. **/ - public val messageCommandsRegistry: ChatCommandRegistry by inject() - - /** Bot settings. **/ - public val botSettings: ExtensibleBotBuilder by inject() - - /** Help extension settings, from the bot builder. **/ - public val settings: ExtensibleBotBuilder.ExtensionsBuilder.HelpExtensionBuilder = - botSettings.extensionsBuilder.helpExtensionBuilder - - override suspend fun setup() { - chatCommand(::HelpArguments) { - name = "extensions.help.commandName" - aliasKey = "extensions.help.commandAliases" - description = "extensions.help.commandDescription" - - localeFallback = true - - check(checks = botSettings.extensionsBuilder.helpExtensionBuilder.checkList.toTypedArray()) - - action { - if (arguments.command.isEmpty()) { - getMainHelpPaginator(this).send() - } else { - getCommandHelpPaginator(this, arguments.command).send() - } - } - } - } - - override suspend fun getMainHelpPaginator(event: MessageCreateEvent, prefix: String): BasePaginator { - var totalCommands = 0 - val locale = event.getLocale() - - val pages = Pages(COMMANDS_GROUP) - val commandPages = gatherCommands(event) - .chunked(HELP_PER_PAGE) - .map { list -> - list.map { - totalCommands += 1 - - formatCommandHelp(prefix, event, it) - } - } - - for (page in commandPages) { - pages.addPage( - COMMANDS_GROUP, - - Page { - description = page.joinToString("\n\n") { "${it.first}\n${it.second}" } - title = translationsProvider.translate("extensions.help.paginator.title.commands", locale) - - footer { - text = translationsProvider.translate( - "extensions.help.paginator.footer", - locale, - replacements = arrayOf(totalCommands) - ) - } - - color = settings.colourGetter(event) - } - ) - - pages.addPage( - ARGUMENTS_GROUP, - - Page { - description = page.joinToString("\n\n") { "${it.first}\n${it.third}" } - title = translationsProvider.translate("extensions.help.paginator.title.arguments", locale) - - footer { - text = translationsProvider.translate( - "extensions.help.paginator.footer", - locale, - replacements = arrayOf(totalCommands) - ) - } - - color = settings.colourGetter(event) - } - ) - } - - if (totalCommands < 1) { - // This should never happen in most cases, but it's best to be safe about it - - pages.addPage( - COMMANDS_GROUP, - Page { - description = translationsProvider.translate("extensions.help.paginator.noCommands", locale) - title = translationsProvider.translate("extensions.help.paginator.noCommands", locale) - footer { - text = translationsProvider.translate( - "extensions.help.paginator.footer", - locale, - replacements = arrayOf(0) - ) - } - color = settings.colourGetter(event) - } - ) - } - - return MessageButtonPaginator( - keepEmbed = settings.deletePaginatorOnTimeout.not(), - locale = locale, - owner = event.message.author, - pages = pages, - pingInReply = settings.pingInReply, - targetMessage = event.message, - timeoutSeconds = settings.paginatorTimeout, - ).onTimeout { - if (settings.deleteInvocationOnPaginatorTimeout) { - @Suppress("TooGenericExceptionCaught") - try { - event.message.deleteIgnoringNotFound() - } catch (t: Throwable) { - logger.warn(t) { "Failed to delete command invocation." } - } - } - } - } - - override suspend fun getCommandHelpPaginator( - event: MessageCreateEvent, - prefix: String, - args: List, - ): BasePaginator = getCommandHelpPaginator(event, prefix, getCommand(event, args)) - - override suspend fun getCommandHelpPaginator( - context: ChatCommandContext<*>, - args: List, - ): BasePaginator = - getCommandHelpPaginator(context, getCommand(context.event, args)) - - override suspend fun getCommandHelpPaginator( - event: MessageCreateEvent, - prefix: String, - command: ChatCommand?, - ): BasePaginator { - val pages = Pages(COMMANDS_GROUP) - val locale = event.getLocale() - - if (command == null || !command.runChecks(event, false, mutableMapOf())) { - pages.addPage( - COMMANDS_GROUP, - - Page { - color = settings.colourGetter(event) - - description = translationsProvider.translate( - "extensions.help.error.missingCommandDescription", - locale - ) - - title = translationsProvider.translate( - "extensions.help.error.missingCommandTitle", - locale - ) - } - ) - } else { - val (openingLine, desc, arguments) = formatCommandHelp(prefix, event, command, longDescription = true) - - val commandName = when (command) { - is ChatSubCommand -> command.getFullTranslatedName(locale) - is ChatGroupCommand -> command.getFullTranslatedName(locale) - else -> command.getTranslatedName(locale) - } - - pages.addPage( - COMMANDS_GROUP, - - Page { - color = settings.colourGetter(event) - description = "$openingLine\n$desc\n\n$arguments" - - title = translationsProvider.translate( - "extensions.help.paginator.title.command", - locale, - replacements = arrayOf(commandName) - ) - } - ) - } - - return MessageButtonPaginator( - keepEmbed = settings.deletePaginatorOnTimeout.not(), - locale = locale, - owner = event.message.author, - pages = pages, - pingInReply = settings.pingInReply, - targetMessage = event.message, - timeoutSeconds = settings.paginatorTimeout, - ).onTimeout { - if (settings.deleteInvocationOnPaginatorTimeout) { - @Suppress("TooGenericExceptionCaught") - try { - event.message.deleteIgnoringNotFound() - } catch (t: Throwable) { - logger.warn(t) { "Failed to delete command invocation." } - } - } - } - } - - override suspend fun gatherCommands(event: MessageCreateEvent): List> = - messageCommandsRegistry.commands - .filter { !it.hidden && it.enabled && it.runChecks(event, false, mutableMapOf()) } - .sortedBy { it.name } - - override suspend fun formatCommandHelp( - prefix: String, - event: MessageCreateEvent, - command: ChatCommand, - longDescription: Boolean, - ): Triple { - val locale = event.getLocale() - val defaultLocale = botSettings.i18nBuilder.defaultLocale - - val commandName = when (command) { - is ChatSubCommand -> command.getFullTranslatedName(locale) - is ChatGroupCommand -> command.getFullTranslatedName(locale) - else -> command.getTranslatedName(locale) - } - - val openingLine = "**$prefix$commandName ${command.getSignature(locale)}**\n" - - val description = buildString { - if (longDescription) { - append(translationsProvider.translate(command.description, command.extension.bundle, locale)) - } else { - append( - translationsProvider.translate(command.description, command.extension.bundle, locale) - .trim() - .takeWhile { it != '\n' } - ) - } - - append("\n") - - val aliases: MutableSet = mutableSetOf() - - aliases.addAll(command.getTranslatedAliases(locale)) - - if (command.localeFallback && locale != defaultLocale) { - aliases.add(command.getTranslatedName(defaultLocale)) - aliases.addAll(command.getTranslatedAliases(defaultLocale)) - - aliases.remove(command.getTranslatedName(locale)) - } - - if (aliases.isNotEmpty()) { - append("\n") - - append( - translationsProvider.translate( - "extensions.help.commandDescription.aliases", - locale - ) - ) - - append(" ") - append( - aliases.sorted().joinToString(", ") { - "`$it`" - } - ) - } - - if (command is ChatGroupCommand) { - val subCommands = command.commands.filter { it.runChecks(event, false, mutableMapOf()) } - - if (subCommands.isNotEmpty()) { - append("\n") - - append( - translationsProvider.translate( - "extensions.help.commandDescription.subCommands", - locale - ) - ) - - append(" ") - append( - subCommands.map { it.getTranslatedName(locale) }.joinToString(", ") { - "`$it`" - } - ) - } - } - - if (command.requiredPerms.isNotEmpty()) { - append("\n") - - append( - translationsProvider.translate( - "extensions.help.commandDescription.requiredBotPermissions", - locale - ) - ) - - append(" ") - append(command.requiredPerms.joinToString(", ") { it.translate(locale) }) - } - }.trim('\n') - - val arguments = buildString { - append("\n\n") - - if (command.arguments == null) { - append( - translationsProvider.translate( - "extensions.help.commandDescription.noArguments", - locale - ) - ) - } else { - @Suppress("TooGenericExceptionCaught") // Hard to say really - try { - val argsObj = command.arguments!!.invoke() - - append( - argsObj.args.joinToString("\n") { - buildString { - append("**»** `${it.displayName}") - - if (it.converter.showTypeInSignature) { - append(" (") - - append( - translationsProvider.translate( - it.converter.signatureTypeString, - it.converter.bundle, - locale - ) - ) - - append(")") - } - - append("`: ") - append(translationsProvider.translate(it.description, command.extension.bundle, locale)) - } - } - ) - } catch (t: Throwable) { - logger.error(t) { "Failed to retrieve argument list for command: $name" } - - append( - translationsProvider.translate( - "extensions.help.commandDescription.error.argumentList", - locale - ) - ) - } - } - }.trim('\n') - - return Triple(openingLine.trim('\n'), description, arguments) - } - - override suspend fun getCommand( - event: MessageCreateEvent, - args: List, - ): ChatCommand? { - val firstArg = args.first() - var command = messageCommandsRegistry.getCommand(firstArg, event) - - if (command?.runChecks(event, false, mutableMapOf()) == false) { - return null - } - - args.drop(1).forEach { - if (command is ChatGroupCommand) { - val gc = command as ChatGroupCommand - - command = if (gc.runChecks(event, false, mutableMapOf())) { - gc.getCommand(it, event) - } else { - null - } - } - } - - return command - } - - /** Help command arguments class. **/ - public class HelpArguments : Arguments() { + override val name: String = "help" + + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by inject() + + /** Message command registry. **/ + public val messageCommandsRegistry: ChatCommandRegistry by inject() + + /** Bot settings. **/ + public val botSettings: ExtensibleBotBuilder by inject() + + /** Help extension settings, from the bot builder. **/ + public val settings: ExtensibleBotBuilder.ExtensionsBuilder.HelpExtensionBuilder = + botSettings.extensionsBuilder.helpExtensionBuilder + + override suspend fun setup() { + chatCommand(::HelpArguments) { + name = "extensions.help.commandName" + aliasKey = "extensions.help.commandAliases" + description = "extensions.help.commandDescription" + + localeFallback = true + + check(checks = botSettings.extensionsBuilder.helpExtensionBuilder.checkList.toTypedArray()) + + action { + if (arguments.command.isEmpty()) { + getMainHelpPaginator(this).send() + } else { + getCommandHelpPaginator(this, arguments.command).send() + } + } + } + } + + override suspend fun getMainHelpPaginator(event: MessageCreateEvent, prefix: String): BasePaginator { + var totalCommands = 0 + val locale = event.getLocale() + + val pages = Pages(COMMANDS_GROUP) + val commandPages = gatherCommands(event) + .chunked(HELP_PER_PAGE) + .map { list -> + list.map { + totalCommands += 1 + + formatCommandHelp(prefix, event, it) + } + } + + for (page in commandPages) { + pages.addPage( + COMMANDS_GROUP, + + Page { + description = page.joinToString("\n\n") { "${it.first}\n${it.second}" } + title = translationsProvider.translate("extensions.help.paginator.title.commands", locale) + + footer { + text = translationsProvider.translate( + "extensions.help.paginator.footer", + locale, + replacements = arrayOf(totalCommands) + ) + } + + color = settings.colourGetter(event) + } + ) + + pages.addPage( + ARGUMENTS_GROUP, + + Page { + description = page.joinToString("\n\n") { "${it.first}\n${it.third}" } + title = translationsProvider.translate("extensions.help.paginator.title.arguments", locale) + + footer { + text = translationsProvider.translate( + "extensions.help.paginator.footer", + locale, + replacements = arrayOf(totalCommands) + ) + } + + color = settings.colourGetter(event) + } + ) + } + + if (totalCommands < 1) { + // This should never happen in most cases, but it's best to be safe about it + + pages.addPage( + COMMANDS_GROUP, + Page { + description = translationsProvider.translate("extensions.help.paginator.noCommands", locale) + title = translationsProvider.translate("extensions.help.paginator.noCommands", locale) + footer { + text = translationsProvider.translate( + "extensions.help.paginator.footer", + locale, + replacements = arrayOf(0) + ) + } + color = settings.colourGetter(event) + } + ) + } + + return MessageButtonPaginator( + keepEmbed = settings.deletePaginatorOnTimeout.not(), + locale = locale, + owner = event.message.author, + pages = pages, + pingInReply = settings.pingInReply, + targetMessage = event.message, + timeoutSeconds = settings.paginatorTimeout, + ).onTimeout { + if (settings.deleteInvocationOnPaginatorTimeout) { + @Suppress("TooGenericExceptionCaught") + try { + event.message.deleteIgnoringNotFound() + } catch (t: Throwable) { + logger.warn(t) { "Failed to delete command invocation." } + } + } + } + } + + override suspend fun getCommandHelpPaginator( + event: MessageCreateEvent, + prefix: String, + args: List, + ): BasePaginator = getCommandHelpPaginator(event, prefix, getCommand(event, args)) + + override suspend fun getCommandHelpPaginator( + context: ChatCommandContext<*>, + args: List, + ): BasePaginator = + getCommandHelpPaginator(context, getCommand(context.event, args)) + + override suspend fun getCommandHelpPaginator( + event: MessageCreateEvent, + prefix: String, + command: ChatCommand?, + ): BasePaginator { + val pages = Pages(COMMANDS_GROUP) + val locale = event.getLocale() + + if (command == null || !command.runChecks(event, false, mutableMapOf())) { + pages.addPage( + COMMANDS_GROUP, + + Page { + color = settings.colourGetter(event) + + description = translationsProvider.translate( + "extensions.help.error.missingCommandDescription", + locale + ) + + title = translationsProvider.translate( + "extensions.help.error.missingCommandTitle", + locale + ) + } + ) + } else { + val (openingLine, desc, arguments) = formatCommandHelp(prefix, event, command, longDescription = true) + + val commandName = when (command) { + is ChatSubCommand -> command.getFullTranslatedName(locale) + is ChatGroupCommand -> command.getFullTranslatedName(locale) + else -> command.getTranslatedName(locale) + } + + pages.addPage( + COMMANDS_GROUP, + + Page { + color = settings.colourGetter(event) + description = "$openingLine\n$desc\n\n$arguments" + + title = translationsProvider.translate( + "extensions.help.paginator.title.command", + locale, + replacements = arrayOf(commandName) + ) + } + ) + } + + return MessageButtonPaginator( + keepEmbed = settings.deletePaginatorOnTimeout.not(), + locale = locale, + owner = event.message.author, + pages = pages, + pingInReply = settings.pingInReply, + targetMessage = event.message, + timeoutSeconds = settings.paginatorTimeout, + ).onTimeout { + if (settings.deleteInvocationOnPaginatorTimeout) { + @Suppress("TooGenericExceptionCaught") + try { + event.message.deleteIgnoringNotFound() + } catch (t: Throwable) { + logger.warn(t) { "Failed to delete command invocation." } + } + } + } + } + + override suspend fun gatherCommands(event: MessageCreateEvent): List> = + messageCommandsRegistry.commands + .filter { !it.hidden && it.enabled && it.runChecks(event, false, mutableMapOf()) } + .sortedBy { it.name } + + override suspend fun formatCommandHelp( + prefix: String, + event: MessageCreateEvent, + command: ChatCommand, + longDescription: Boolean, + ): Triple { + val locale = event.getLocale() + val defaultLocale = botSettings.i18nBuilder.defaultLocale + + val commandName = when (command) { + is ChatSubCommand -> command.getFullTranslatedName(locale) + is ChatGroupCommand -> command.getFullTranslatedName(locale) + else -> command.getTranslatedName(locale) + } + + val openingLine = "**$prefix$commandName ${command.getSignature(locale)}**\n" + + val description = buildString { + if (longDescription) { + append(translationsProvider.translate(command.description, command.extension.bundle, locale)) + } else { + append( + translationsProvider.translate(command.description, command.extension.bundle, locale) + .trim() + .takeWhile { it != '\n' } + ) + } + + append("\n") + + val aliases: MutableSet = mutableSetOf() + + aliases.addAll(command.getTranslatedAliases(locale)) + + if (command.localeFallback && locale != defaultLocale) { + aliases.add(command.getTranslatedName(defaultLocale)) + aliases.addAll(command.getTranslatedAliases(defaultLocale)) + + aliases.remove(command.getTranslatedName(locale)) + } + + if (aliases.isNotEmpty()) { + append("\n") + + append( + translationsProvider.translate( + "extensions.help.commandDescription.aliases", + locale + ) + ) + + append(" ") + append( + aliases.sorted().joinToString(", ") { + "`$it`" + } + ) + } + + if (command is ChatGroupCommand) { + val subCommands = command.commands.filter { it.runChecks(event, false, mutableMapOf()) } + + if (subCommands.isNotEmpty()) { + append("\n") + + append( + translationsProvider.translate( + "extensions.help.commandDescription.subCommands", + locale + ) + ) + + append(" ") + append( + subCommands.map { it.getTranslatedName(locale) }.joinToString(", ") { + "`$it`" + } + ) + } + } + + if (command.requiredPerms.isNotEmpty()) { + append("\n") + + append( + translationsProvider.translate( + "extensions.help.commandDescription.requiredBotPermissions", + locale + ) + ) + + append(" ") + append(command.requiredPerms.joinToString(", ") { it.translate(locale) }) + } + }.trim('\n') + + val arguments = buildString { + append("\n\n") + + if (command.arguments == null) { + append( + translationsProvider.translate( + "extensions.help.commandDescription.noArguments", + locale + ) + ) + } else { + @Suppress("TooGenericExceptionCaught") // Hard to say really + try { + val argsObj = command.arguments!!.invoke() + + append( + argsObj.args.joinToString("\n") { + buildString { + append("**»** `${it.displayName}") + + if (it.converter.showTypeInSignature) { + append(" (") + + append( + translationsProvider.translate( + it.converter.signatureTypeString, + it.converter.bundle, + locale + ) + ) + + append(")") + } + + append("`: ") + append(translationsProvider.translate(it.description, command.extension.bundle, locale)) + } + } + ) + } catch (t: Throwable) { + logger.error(t) { "Failed to retrieve argument list for command: $name" } + + append( + translationsProvider.translate( + "extensions.help.commandDescription.error.argumentList", + locale + ) + ) + } + } + }.trim('\n') + + return Triple(openingLine.trim('\n'), description, arguments) + } + + override suspend fun getCommand( + event: MessageCreateEvent, + args: List, + ): ChatCommand? { + val firstArg = args.first() + var command = messageCommandsRegistry.getCommand(firstArg, event) + + if (command?.runChecks(event, false, mutableMapOf()) == false) { + return null + } + + args.drop(1).forEach { + if (command is ChatGroupCommand) { + val gc = command as ChatGroupCommand + + command = if (gc.runChecks(event, false, mutableMapOf())) { + gc.getCommand(it, event) + } else { + null + } + } + } + + return command + } + + /** Help command arguments class. **/ + public class HelpArguments : Arguments() { // public val command: List by stringList( // "command", // "extensions.help.commandArguments.command", // false // ) - /** Command to get help for. **/ - public val command: List by stringList { - name = "command" - description = "extensions.help.commandArguments.command" - } - } + /** Command to get help for. **/ + public val command: List by stringList { + name = "command" + description = "extensions.help.commandArguments.command" + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/impl/SentryExtension.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/impl/SentryExtension.kt index 5298172bb8..be9f16673a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/impl/SentryExtension.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/extensions/impl/SentryExtension.kt @@ -27,112 +27,112 @@ import org.koin.core.component.inject * Even if you add this extension manually, it won't do anything unless you've set up the Sentry integration. */ public class SentryExtension : Extension() { - override val name: String = "sentry" + override val name: String = "sentry" - /** Sentry adapter, for easy access to Sentry functions. **/ - public val sentryAdapter: SentryAdapter by inject() + /** Sentry adapter, for easy access to Sentry functions. **/ + public val sentryAdapter: SentryAdapter by inject() - /** Bot settings. **/ - public val botSettings: ExtensibleBotBuilder by inject() + /** Bot settings. **/ + public val botSettings: ExtensibleBotBuilder by inject() - /** Sentry extension settings, from the bot builder. **/ - public val sentrySettings: ExtensibleBotBuilder.ExtensionsBuilder.SentryExtensionBuilder = - botSettings.extensionsBuilder.sentryExtensionBuilder + /** Sentry extension settings, from the bot builder. **/ + public val sentrySettings: ExtensibleBotBuilder.ExtensionsBuilder.SentryExtensionBuilder = + botSettings.extensionsBuilder.sentryExtensionBuilder - @Suppress("StringLiteralDuplication") // It's the command name - override suspend fun setup() { - if (sentryAdapter.enabled) { - ephemeralSlashCommand(::FeedbackSlashArgs) { - name = "extensions.sentry.commandName" - description = "extensions.sentry.commandDescription.short" + @Suppress("StringLiteralDuplication") // It's the command name + override suspend fun setup() { + if (sentryAdapter.enabled) { + ephemeralSlashCommand(::FeedbackSlashArgs) { + name = "extensions.sentry.commandName" + description = "extensions.sentry.commandDescription.short" - action { - if (!sentry.adapter.hasEventId(arguments.id)) { - respond { - content = translate("extensions.sentry.error.invalidId") - } + action { + if (!sentry.adapter.hasEventId(arguments.id)) { + respond { + content = translate("extensions.sentry.error.invalidId") + } - return@action - } + return@action + } - val feedback = UserFeedback( - arguments.id, - member!!.asMember().tag, - member!!.id.toString(), - arguments.feedback - ) + val feedback = UserFeedback( + arguments.id, + member!!.asMember().tag, + member!!.id.toString(), + arguments.feedback + ) sentry.captureFeedback(feedback) sentry.adapter.removeEventId(arguments.id) - respond { - content = translate("extensions.sentry.thanks") - } - } - } - - chatCommand(::FeedbackMessageArgs) { - name = "extensions.sentry.commandName" - description = "extensions.sentry.commandDescription.long" - - aliasKey = "extensions.sentry.commandAliases" - - action { - if (!sentry.adapter.hasEventId(arguments.id)) { - message.respond( - translate("extensions.sentry.error.invalidId"), - pingInReply = sentrySettings.pingInReply - ) - - return@action - } - - val author = message.author!! - val feedback = UserFeedback( - arguments.id, - author.tag, - author.id.toString(), - arguments.feedback - ) - - Sentry.captureUserFeedback(feedback) + respond { + content = translate("extensions.sentry.thanks") + } + } + } + + chatCommand(::FeedbackMessageArgs) { + name = "extensions.sentry.commandName" + description = "extensions.sentry.commandDescription.long" + + aliasKey = "extensions.sentry.commandAliases" + + action { + if (!sentry.adapter.hasEventId(arguments.id)) { + message.respond( + translate("extensions.sentry.error.invalidId"), + pingInReply = sentrySettings.pingInReply + ) + + return@action + } + + val author = message.author!! + val feedback = UserFeedback( + arguments.id, + author.tag, + author.id.toString(), + arguments.feedback + ) + + Sentry.captureUserFeedback(feedback) sentry.adapter.removeEventId(arguments.id) - message.respond( - translate("extensions.sentry.thanks") - ) - } - } - } - } - - /** Arguments for the feedback command. **/ - public class FeedbackMessageArgs : Arguments() { - /** Sentry event ID. **/ - public val id: SentryId by sentryId { + message.respond( + translate("extensions.sentry.thanks") + ) + } + } + } + } + + /** Arguments for the feedback command. **/ + public class FeedbackMessageArgs : Arguments() { + /** Sentry event ID. **/ + public val id: SentryId by sentryId { name = "id" description = "extensions.sentry.arguments.id" } - /** Feedback message to submit to Sentry. **/ - public val feedback: String by coalescingString { - name = "feedback" - description = "extensions.sentry.arguments.feedback" - } - } - - /** Arguments for the feedback command. **/ - public class FeedbackSlashArgs : Arguments() { - /** Sentry event ID. **/ - public val id: SentryId by sentryId { + /** Feedback message to submit to Sentry. **/ + public val feedback: String by coalescingString { + name = "feedback" + description = "extensions.sentry.arguments.feedback" + } + } + + /** Arguments for the feedback command. **/ + public class FeedbackSlashArgs : Arguments() { + /** Sentry event ID. **/ + public val id: SentryId by sentryId { name = "id" description = "extensions.sentry.arguments.id" } - /** Feedback message to submit to Sentry. **/ - public val feedback: String by string { - name = "feedback" - description = "extensions.sentry.arguments.feedback" - } - } + /** Feedback message to submit to Sentry. **/ + public val feedback: String by string { + name = "feedback" + description = "extensions.sentry.arguments.feedback" + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/ResourceBundleTranslations.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/ResourceBundleTranslations.kt index de738af64d..636313e1b5 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/ResourceBundleTranslations.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/ResourceBundleTranslations.kt @@ -28,179 +28,180 @@ import java.util.* * to `translations/kordex/strings${_locale ?: ""}.properties` in the resources. */ public open class ResourceBundleTranslations( - defaultLocaleBuilder: () -> Locale, + defaultLocaleBuilder: () -> Locale, ) : TranslationsProvider(defaultLocaleBuilder) { - private val logger: KLogger = KotlinLogging.logger( - "com.kotlindiscord.kord.extensions.i18n.ResourceBundleTranslations" - ) - - private val bundles: MutableMap, ResourceBundle> = mutableMapOf() - private val overrideBundles: MutableMap, ResourceBundle> = mutableMapOf() - - public override fun hasKey(key: String, locale: Locale, bundleName: String?): Boolean { - return try { - val (bundle, _) = getBundles(locale, bundleName) - - // Overrides aren't for adding keys, so we don't check them - bundle.keys.toList().contains(key) - } catch (e: MissingResourceException) { - logger.trace { "Failed to get bundle $bundleName for locale $locale" } - - false - } - } - - /** - * Loads the [ResourceBundle] called [bundle] for [locale]. - * - * @see ResourceBundle.getBundle - */ - protected open fun getResourceBundle( - bundle: String, - locale: Locale, - control: ResourceBundle.Control, - ): ResourceBundle = ResourceBundle.getBundle(bundle, locale, control) - - /** - * Retrieves a pair of the [ResourceBundle] and the override resource bundle for [bundleName] in locale. - */ - @Throws(MissingResourceException::class) - protected open fun getBundles(locale: Locale, bundleName: String?): Pair { - val bundle = buildString { - append("translations." + (bundleName ?: KORDEX_KEY)) - - if (this.count { it == '.' } < 2) { - append(".$DEFAULT_BUNDLE_SUFFIX") - } - } - - val bundleKey = bundle to locale - - if (bundles[bundleKey] == null) { - val localeTag = locale.toLanguageTag() - - logger.trace { "Getting bundle $bundle for locale $locale" } - - val firstBundle = getResourceBundle(bundle, locale, Control) - - bundles[bundleKey] = if (localeTag.count { it in "-_" } != 0) { - val baseCode = localeTag.split('-', '_').first() - val secondLocale = Locale(baseCode, baseCode) - val secondBundle = getResourceBundle(bundle, secondLocale, Control) - - val firstKey = firstBundle.keySet().first() - - if (firstBundle.getStringOrNull(firstKey) != secondBundle.getStringOrNull(firstKey)) { - secondBundle - } else { - firstBundle - } - } else { - firstBundle - } - - try { - val overrideBundle = bundle + "_override" - - logger.trace { "Getting override bundle $overrideBundle for locale $locale" } - - overrideBundles[bundleKey] = getResourceBundle(overrideBundle, locale, Control) - } catch (e: MissingResourceException) { - logger.trace { "No override bundle found." } - } - } - - return bundles[bundleKey]!! to overrideBundles[bundleKey] - } - - @Throws(MissingResourceException::class) - public override fun get(key: String, locale: Locale, bundleName: String?): String { - val (bundle, overrideBundle) = getBundles(locale, bundleName) - val result = overrideBundle?.getStringOrNull(key) ?: bundle.getString(key) - - logger.trace { "Result: $key -> $result" } - - return result - } - - /** - * Retrieve a translated string from a [key] in a given [bundleName]. - * - * The string's parameters are not replaced. - */ - protected fun getTranslatedString(key: String, locale: Locale, bundleName: String?): String { - var string = try { - get(key, locale, bundleName) - } catch (e: MissingResourceException) { - key - } - - return try { - if (string == key && bundleName != null) { - // Fall through to the default bundle if the key isn't found - logger.trace { "'$key' not found in bundle '$bundleName' - falling through to '$KORDEX_KEY'" } - - string = get(key, locale, KORDEX_KEY) - } - string - } catch (e: MissingResourceException) { - logger.trace { - if (bundleName == null) { - "Unable to find translation for key '$key' in bundle '$KORDEX_KEY'" - } else { - "Unable to find translation for key '$key' in bundles: '$bundleName', '$KORDEX_KEY'" - } - } - - key - } - } - - override fun translate(key: String, locale: Locale, bundleName: String?, replacements: Array): String { - val string = getTranslatedString(key, locale, bundleName) - - val formatter = MessageFormat(string, locale) - - return formatter.format(replacements) - } - - override fun translate(key: String, locale: Locale, bundleName: String?, replacements: Map): String { - val string = getTranslatedString(key, locale, bundleName) - - val formatter = MessageFormat(string, locale) - - return formatter.format(replacements) - } - - private fun ResourceBundle.getStringOrNull(key: String): String? { - return try { - getString(key) - } catch (e: MissingResourceException) { - null - } - } - - private object Control : ResourceBundle.Control(), KordExKoinComponent { - val builder: ExtensibleBotBuilder by inject() - - override fun getFormats(baseName: String?): MutableList { - if (baseName == null) { - throw NullPointerException() - } - - return FORMAT_PROPERTIES - } - - override fun getFallbackLocale(baseName: String?, locale: Locale?): Locale? { - if (baseName == null) { - throw NullPointerException() - } - - return if (locale == builder.i18nBuilder.defaultLocale) { - null - } else { - builder.i18nBuilder.defaultLocale - } - } - } + private val logger: KLogger = KotlinLogging.logger( + "com.kotlindiscord.kord.extensions.i18n.ResourceBundleTranslations" + ) + + private val bundles: MutableMap, ResourceBundle> = mutableMapOf() + private val overrideBundles: MutableMap, ResourceBundle> = mutableMapOf() + + public override fun hasKey(key: String, locale: Locale, bundleName: String?): Boolean { + return try { + val (bundle, _) = getBundles(locale, bundleName) + + // Overrides aren't for adding keys, so we don't check them + bundle.keys.toList().contains(key) + } catch (e: MissingResourceException) { + logger.trace { "Failed to get bundle $bundleName for locale $locale" } + + false + } + } + + /** + * Loads the [ResourceBundle] called [bundle] for [locale]. + * + * @see ResourceBundle.getBundle + */ + protected open fun getResourceBundle( + bundle: String, + locale: Locale, + control: ResourceBundle.Control, + ): ResourceBundle = ResourceBundle.getBundle(bundle, locale, control) + + /** + * Retrieves a pair of the [ResourceBundle] and the override resource bundle for [bundleName] in locale. + */ + @Throws(MissingResourceException::class) + protected open fun getBundles(locale: Locale, bundleName: String?): Pair { + val bundle = buildString { + append("translations." + (bundleName ?: KORDEX_KEY)) + + if (this.count { it == '.' } < 2) { + append(".$DEFAULT_BUNDLE_SUFFIX") + } + } + + val bundleKey = bundle to locale + + if (bundles[bundleKey] == null) { + val localeTag = locale.toLanguageTag() + + logger.trace { "Getting bundle $bundle for locale $locale" } + + val firstBundle = getResourceBundle(bundle, locale, Control) + + bundles[bundleKey] = if (localeTag.count { it in "-_" } != 0) { + val baseCode = localeTag.split('-', '_').first() + val secondLocale = Locale(baseCode, baseCode) + val secondBundle = getResourceBundle(bundle, secondLocale, Control) + + val firstKey = firstBundle.keySet().first() + + if (firstBundle.getStringOrNull(firstKey) != secondBundle.getStringOrNull(firstKey)) { + secondBundle + } else { + firstBundle + } + } else { + firstBundle + } + + try { + val overrideBundle = bundle + "_override" + + logger.trace { "Getting override bundle $overrideBundle for locale $locale" } + + overrideBundles[bundleKey] = getResourceBundle(overrideBundle, locale, Control) + } catch (e: MissingResourceException) { + logger.trace { "No override bundle found." } + } + } + + return bundles[bundleKey]!! to overrideBundles[bundleKey] + } + + @Throws(MissingResourceException::class) + public override fun get(key: String, locale: Locale, bundleName: String?): String { + val (bundle, overrideBundle) = getBundles(locale, bundleName) + val result = overrideBundle?.getStringOrNull(key) ?: bundle.getString(key) + + logger.trace { "Result: $key -> $result" } + + return result + } + + /** + * Retrieve a translated string from a [key] in a given [bundleName]. + * + * The string's parameters are not replaced. + */ + protected fun getTranslatedString(key: String, locale: Locale, bundleName: String?): String { + var string = try { + get(key, locale, bundleName) + } catch (e: MissingResourceException) { + key + } + + return try { + if (string == key && bundleName != null) { + // Fall through to the default bundle if the key isn't found + logger.trace { "'$key' not found in bundle '$bundleName' - falling through to '$KORDEX_KEY'" } + + string = get(key, locale, KORDEX_KEY) + } + + string + } catch (e: MissingResourceException) { + logger.trace { + if (bundleName == null) { + "Unable to find translation for key '$key' in bundle '$KORDEX_KEY'" + } else { + "Unable to find translation for key '$key' in bundles: '$bundleName', '$KORDEX_KEY'" + } + } + + key + } + } + + override fun translate(key: String, locale: Locale, bundleName: String?, replacements: Array): String { + val string = getTranslatedString(key, locale, bundleName) + + val formatter = MessageFormat(string, locale) + + return formatter.format(replacements) + } + + override fun translate(key: String, locale: Locale, bundleName: String?, replacements: Map): String { + val string = getTranslatedString(key, locale, bundleName) + + val formatter = MessageFormat(string, locale) + + return formatter.format(replacements) + } + + private fun ResourceBundle.getStringOrNull(key: String): String? { + return try { + getString(key) + } catch (e: MissingResourceException) { + null + } + } + + private object Control : ResourceBundle.Control(), KordExKoinComponent { + val builder: ExtensibleBotBuilder by inject() + + override fun getFormats(baseName: String?): MutableList { + if (baseName == null) { + throw NullPointerException() + } + + return FORMAT_PROPERTIES + } + + override fun getFallbackLocale(baseName: String?, locale: Locale?): Locale? { + if (baseName == null) { + throw NullPointerException() + } + + return if (locale == builder.i18nBuilder.defaultLocale) { + null + } else { + builder.i18nBuilder.defaultLocale + } + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/SupportedLocales.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/SupportedLocales.kt index 32b817bb4c..adaab38951 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/SupportedLocales.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/SupportedLocales.kt @@ -15,121 +15,121 @@ import java.util.* * back: https://hosted.weblate.org/projects/kord-extensions/main/ */ public object SupportedLocales { - /** Simplified Chinese locale. **/ - public val CHINESE_SIMPLIFIED: Locale = Locale("zh", "cn") - - /** English locale. **/ - public val ENGLISH: Locale = Locale("en", "gb") - - /** Finnish locale. **/ - public val FINNISH: Locale = Locale("fi", "fi") - - /** French locale. **/ - public val FRENCH: Locale = Locale("fr", "fr") - - /** German locale. **/ - public val GERMAN: Locale = Locale("de", "de") - - /** Korean locale. **/ - public val KOREAN: Locale = Locale("ko") - - /** Polish locale. **/ - public val POLISH: Locale = Locale("pl", "pl") - - /** Portuguese locale. **/ - public val PORTUGUESE: Locale = Locale("pt", "pt") - - /** Russian locale. **/ - public val RUSSIAN: Locale = Locale("ru", "ru") - - /** Spanish locale. **/ - public val SPANISH: Locale = Locale("es") - - /** Toki Pona locale. **/ - public val TOKI_PONA: Locale = Locale("tok") - - /** Turkish locale. **/ - public val TURKISH: Locale = Locale("tr") - - /** Map of string names to supported Locale objects.. **/ - public val ALL_LOCALES: Map = mapOf( - "chinese" to CHINESE_SIMPLIFIED, - "zh" to CHINESE_SIMPLIFIED, - "zh_cn" to CHINESE_SIMPLIFIED, - "中文" to CHINESE_SIMPLIFIED, - "普通话" to CHINESE_SIMPLIFIED, - "汉语" to CHINESE_SIMPLIFIED, - "简体中文" to CHINESE_SIMPLIFIED, - - "en" to ENGLISH, - "en_gb" to ENGLISH, - "en_us" to ENGLISH, - "english" to ENGLISH, - - "es" to SPANISH, - "es_es" to SPANISH, - "spanish" to SPANISH, - "espanol" to SPANISH, - "espanola" to SPANISH, - "español" to SPANISH, - "española" to SPANISH, - - "fi" to FINNISH, - "fi_fi" to FINNISH, - "finnish" to FINNISH, - "suomen kieli" to FINNISH, - "suomen" to FINNISH, - "suomi" to FINNISH, - - "fr" to FRENCH, - "fr_fr" to FRENCH, - "francais" to FRENCH, - "français" to FRENCH, - "french" to FRENCH, - - "de" to GERMAN, - "de_de" to GERMAN, - "deutsch" to GERMAN, - "german" to GERMAN, - - "ko" to KOREAN, - "ko_ko" to KOREAN, - "korean" to KOREAN, - "한국어" to KOREAN, - - "portugues" to PORTUGUESE, - "portuguese" to PORTUGUESE, - "português" to PORTUGUESE, - "pt" to PORTUGUESE, - "pt_pt" to PORTUGUESE, - - "pl" to POLISH, - "pl_pl" to POLISH, - "polish" to POLISH, - "polska" to POLISH, - "polskie" to POLISH, - - "ru" to RUSSIAN, - "ru_ru" to RUSSIAN, - "russian" to RUSSIAN, - "русская" to RUSSIAN, - "русскии" to RUSSIAN, - "русский" to RUSSIAN, - - "tok" to TOKI_PONA, - "tok_tok" to TOKI_PONA, - "toki" to TOKI_PONA, - "toki_pona" to TOKI_PONA, - "toki pona" to TOKI_PONA, - - "tr" to TURKISH, - "tr_tr" to TURKISH, - "turkish" to TURKISH, - "turkce" to TURKISH, - "turkçe" to TURKISH, - "türkce" to TURKISH, - "türkçe" to TURKISH, - ) + /** Simplified Chinese locale. **/ + public val CHINESE_SIMPLIFIED: Locale = Locale("zh", "cn") + + /** English locale. **/ + public val ENGLISH: Locale = Locale("en", "gb") + + /** Finnish locale. **/ + public val FINNISH: Locale = Locale("fi", "fi") + + /** French locale. **/ + public val FRENCH: Locale = Locale("fr", "fr") + + /** German locale. **/ + public val GERMAN: Locale = Locale("de", "de") + + /** Korean locale. **/ + public val KOREAN: Locale = Locale("ko") + + /** Polish locale. **/ + public val POLISH: Locale = Locale("pl", "pl") + + /** Portuguese locale. **/ + public val PORTUGUESE: Locale = Locale("pt", "pt") + + /** Russian locale. **/ + public val RUSSIAN: Locale = Locale("ru", "ru") + + /** Spanish locale. **/ + public val SPANISH: Locale = Locale("es") + + /** Toki Pona locale. **/ + public val TOKI_PONA: Locale = Locale("tok") + + /** Turkish locale. **/ + public val TURKISH: Locale = Locale("tr") + + /** Map of string names to supported Locale objects.. **/ + public val ALL_LOCALES: Map = mapOf( + "chinese" to CHINESE_SIMPLIFIED, + "zh" to CHINESE_SIMPLIFIED, + "zh_cn" to CHINESE_SIMPLIFIED, + "中文" to CHINESE_SIMPLIFIED, + "普通话" to CHINESE_SIMPLIFIED, + "汉语" to CHINESE_SIMPLIFIED, + "简体中文" to CHINESE_SIMPLIFIED, + + "en" to ENGLISH, + "en_gb" to ENGLISH, + "en_us" to ENGLISH, + "english" to ENGLISH, + + "es" to SPANISH, + "es_es" to SPANISH, + "spanish" to SPANISH, + "espanol" to SPANISH, + "espanola" to SPANISH, + "español" to SPANISH, + "española" to SPANISH, + + "fi" to FINNISH, + "fi_fi" to FINNISH, + "finnish" to FINNISH, + "suomen kieli" to FINNISH, + "suomen" to FINNISH, + "suomi" to FINNISH, + + "fr" to FRENCH, + "fr_fr" to FRENCH, + "francais" to FRENCH, + "français" to FRENCH, + "french" to FRENCH, + + "de" to GERMAN, + "de_de" to GERMAN, + "deutsch" to GERMAN, + "german" to GERMAN, + + "ko" to KOREAN, + "ko_ko" to KOREAN, + "korean" to KOREAN, + "한국어" to KOREAN, + + "portugues" to PORTUGUESE, + "portuguese" to PORTUGUESE, + "português" to PORTUGUESE, + "pt" to PORTUGUESE, + "pt_pt" to PORTUGUESE, + + "pl" to POLISH, + "pl_pl" to POLISH, + "polish" to POLISH, + "polska" to POLISH, + "polskie" to POLISH, + + "ru" to RUSSIAN, + "ru_ru" to RUSSIAN, + "russian" to RUSSIAN, + "русская" to RUSSIAN, + "русскии" to RUSSIAN, + "русский" to RUSSIAN, + + "tok" to TOKI_PONA, + "tok_tok" to TOKI_PONA, + "toki" to TOKI_PONA, + "toki_pona" to TOKI_PONA, + "toki pona" to TOKI_PONA, + + "tr" to TURKISH, + "tr_tr" to TURKISH, + "turkish" to TURKISH, + "turkce" to TURKISH, + "turkçe" to TURKISH, + "türkce" to TURKISH, + "türkçe" to TURKISH, + ) /** Set containing all supported locales. **/ public val ALL_LOCALES_SET: Set = ALL_LOCALES.values.toSet() diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/TranslationsProvider.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/TranslationsProvider.kt index e66f6a5581..77af8774fe 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/TranslationsProvider.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/i18n/TranslationsProvider.kt @@ -15,144 +15,144 @@ import java.util.* * the first time it's accessed. */ public abstract class TranslationsProvider( - public open val defaultLocaleBuilder: () -> Locale + public open val defaultLocaleBuilder: () -> Locale, ) { - /** - * Default locale, resolved via [defaultLocaleBuilder]. Avoid accessing this outside of your [get] functions, as - * accessing it too early will prevent the user from configuring it properly. - */ - public open val defaultLocale: Locale by lazy { defaultLocaleBuilder() } - - /** Check whether a translation key exists in the given bundle and locale. **/ - public abstract fun hasKey(key: String, locale: Locale, bundleName: String?): Boolean - - /** Get a translation by key from the given bundle name (`kordex.strings` by default). **/ - public open fun get(key: String, bundleName: String? = null): String = - get(key, defaultLocale, bundleName) - - /** Get a translation by key from the given locale and bundle name (`kordex.strings` by default). **/ - public abstract fun get(key: String, locale: Locale, bundleName: String? = null): String - - /** Get a translation by key from the given language code and bundle name (`kordex.strings` by default). **/ - public open fun get(key: String, language: String, bundleName: String? = null): String = - get(key, Locale(language), bundleName) - - /** - * Get a translation by key from the given language and country codes, and bundle name (`kordex.strings` by - * default). - */ - public open fun get(key: String, language: String, country: String, bundleName: String? = null): String = - get(key, Locale(language, country), bundleName) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - bundleName: String? = null, - replacements: Array = arrayOf() - ): String = translate(key, defaultLocale, bundleName, replacements) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - bundleName: String? = null, - locale: Locale, - replacements: Array = arrayOf() - ): String = translate(key, locale, bundleName, replacements) - - /** Get a formatted translation using the provided arguments. **/ - public abstract fun translate( - key: String, - locale: Locale, - bundleName: String? = null, - replacements: Array = arrayOf() - ): String - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - language: String, - bundleName: String? = null, - replacements: Array = arrayOf() - ): String = translate(key, Locale(language), bundleName, replacements) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - language: String, - country: String, - bundleName: String? = null, - replacements: Array = arrayOf() - ): String = translate(key, Locale(language, country), bundleName, replacements) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - bundleName: String? = null, - replacements: List - ): String = translate(key, bundleName, replacements.toTypedArray()) - - /** Get a formatted translation using the provided arguments. **/ - public fun translate( - key: String, - locale: Locale, - bundleName: String? = null, - replacements: List - ): String = translate(key, locale, bundleName, replacements.toTypedArray()) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - language: String, - bundleName: String? = null, - replacements: List - ): String = translate(key, language, bundleName, replacements.toTypedArray()) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - language: String, - country: String, - bundleName: String? = null, - replacements: List - ): String = translate(key, language, country, bundleName, replacements.toTypedArray()) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - bundleName: String? = null, - replacements: Map - ): String = translate(key, defaultLocale, bundleName, replacements) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - bundleName: String? = null, - locale: Locale, - replacements: Map - ): String = translate(key, locale, bundleName, replacements) - - /** Get a formatted translation using the provided arguments. **/ - public abstract fun translate( - key: String, - locale: Locale, - bundleName: String? = null, - replacements: Map - ): String - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - language: String, - bundleName: String? = null, - replacements: Map - ): String = translate(key, Locale(language), bundleName, replacements) - - /** Get a formatted translation using the provided arguments. **/ - public open fun translate( - key: String, - language: String, - country: String, - bundleName: String? = null, - replacements: Map - ): String = translate(key, Locale(language, country), bundleName, replacements) + /** + * Default locale, resolved via [defaultLocaleBuilder]. Avoid accessing this outside of your [get] functions, as + * accessing it too early will prevent the user from configuring it properly. + */ + public open val defaultLocale: Locale by lazy { defaultLocaleBuilder() } + + /** Check whether a translation key exists in the given bundle and locale. **/ + public abstract fun hasKey(key: String, locale: Locale, bundleName: String?): Boolean + + /** Get a translation by key from the given bundle name (`kordex.strings` by default). **/ + public open fun get(key: String, bundleName: String? = null): String = + get(key, defaultLocale, bundleName) + + /** Get a translation by key from the given locale and bundle name (`kordex.strings` by default). **/ + public abstract fun get(key: String, locale: Locale, bundleName: String? = null): String + + /** Get a translation by key from the given language code and bundle name (`kordex.strings` by default). **/ + public open fun get(key: String, language: String, bundleName: String? = null): String = + get(key, Locale(language), bundleName) + + /** + * Get a translation by key from the given language and country codes, and bundle name (`kordex.strings` by + * default). + */ + public open fun get(key: String, language: String, country: String, bundleName: String? = null): String = + get(key, Locale(language, country), bundleName) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + bundleName: String? = null, + replacements: Array = arrayOf(), + ): String = translate(key, defaultLocale, bundleName, replacements) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + bundleName: String? = null, + locale: Locale, + replacements: Array = arrayOf(), + ): String = translate(key, locale, bundleName, replacements) + + /** Get a formatted translation using the provided arguments. **/ + public abstract fun translate( + key: String, + locale: Locale, + bundleName: String? = null, + replacements: Array = arrayOf(), + ): String + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + language: String, + bundleName: String? = null, + replacements: Array = arrayOf(), + ): String = translate(key, Locale(language), bundleName, replacements) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + language: String, + country: String, + bundleName: String? = null, + replacements: Array = arrayOf(), + ): String = translate(key, Locale(language, country), bundleName, replacements) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + bundleName: String? = null, + replacements: List, + ): String = translate(key, bundleName, replacements.toTypedArray()) + + /** Get a formatted translation using the provided arguments. **/ + public fun translate( + key: String, + locale: Locale, + bundleName: String? = null, + replacements: List, + ): String = translate(key, locale, bundleName, replacements.toTypedArray()) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + language: String, + bundleName: String? = null, + replacements: List, + ): String = translate(key, language, bundleName, replacements.toTypedArray()) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + language: String, + country: String, + bundleName: String? = null, + replacements: List, + ): String = translate(key, language, country, bundleName, replacements.toTypedArray()) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + bundleName: String? = null, + replacements: Map, + ): String = translate(key, defaultLocale, bundleName, replacements) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + bundleName: String? = null, + locale: Locale, + replacements: Map, + ): String = translate(key, locale, bundleName, replacements) + + /** Get a formatted translation using the provided arguments. **/ + public abstract fun translate( + key: String, + locale: Locale, + bundleName: String? = null, + replacements: Map, + ): String + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + language: String, + bundleName: String? = null, + replacements: Map, + ): String = translate(key, Locale(language), bundleName, replacements) + + /** Get a formatted translation using the provided arguments. **/ + public open fun translate( + key: String, + language: String, + country: String, + bundleName: String? = null, + replacements: Map, + ): String = translate(key, Locale(language, country), bundleName, replacements) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/koin/KordExKoinComponent.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/koin/KordExKoinComponent.kt index b685a39c7b..76404bf0e3 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/koin/KordExKoinComponent.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/koin/KordExKoinComponent.kt @@ -13,10 +13,10 @@ import org.koin.core.component.KoinComponent * [KoinComponent] that gives access to dependencies from Koin app within [KordExContext]. */ public interface KordExKoinComponent : KoinComponent { - /** - * Get the associated Koin instance. - * - * @throws IllegalStateException KoinApplication not yet started. - */ - override fun getKoin(): Koin = KordExContext.get() + /** + * Get the associated Koin instance. + * + * @throws IllegalStateException KoinApplication not yet started. + */ + override fun getKoin(): Koin = KordExContext.get() } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/EphemeralResponsePaginator.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/EphemeralResponsePaginator.kt index 83a54384e0..6ff23c83fc 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/EphemeralResponsePaginator.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/EphemeralResponsePaginator.kt @@ -32,58 +32,58 @@ public class EphemeralResponsePaginator( public val interaction: EphemeralMessageInteractionResponseBehavior, ) : BaseButtonPaginator(pages, chunkedPages, owner, timeoutSeconds, true, switchEmoji, mutator, bundle, locale) { - /** Whether this paginator has been set up for the first time. **/ - public var isSetup: Boolean = false + /** Whether this paginator has been set up for the first time. **/ + public var isSetup: Boolean = false - override suspend fun send() { - if (!isSetup) { - isSetup = true + override suspend fun send() { + if (!isSetup) { + isSetup = true - setup() - } else { - updateButtons() - } + setup() + } else { + updateButtons() + } - interaction.edit { + interaction.edit { applyPage() - with(this@EphemeralResponsePaginator.components) { - this@edit.applyToMessage() - } - } - } + with(this@EphemeralResponsePaginator.components) { + this@edit.applyToMessage() + } + } + } - override suspend fun destroy() { - if (!active) { - return - } + override suspend fun destroy() { + if (!active) { + return + } - active = false + active = false - interaction.edit { + interaction.edit { applyPage() - this.components = mutableListOf() - } + this.components = mutableListOf() + } - super.destroy() - } + super.destroy() + } } /** Convenience function for creating an interaction button paginator from a paginator builder. **/ @Suppress("FunctionNaming") // Factory function public fun EphemeralResponsePaginator( - builder: PaginatorBuilder, - interaction: EphemeralMessageInteractionResponseBehavior + builder: PaginatorBuilder, + interaction: EphemeralMessageInteractionResponseBehavior, ): EphemeralResponsePaginator = EphemeralResponsePaginator( - pages = builder.pages, + pages = builder.pages, chunkedPages = builder.chunkedPages, - owner = builder.owner, - timeoutSeconds = builder.timeoutSeconds, + owner = builder.owner, + timeoutSeconds = builder.timeoutSeconds, mutator = builder.mutator, - bundle = builder.bundle, - locale = builder.locale, - interaction = interaction, + bundle = builder.bundle, + locale = builder.locale, + interaction = interaction, - switchEmoji = builder.switchEmoji ?: if (builder.pages.groups.size == 2) EXPAND_EMOJI else SWITCH_EMOJI, + switchEmoji = builder.switchEmoji ?: if (builder.pages.groups.size == 2) EXPAND_EMOJI else SWITCH_EMOJI, ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/MessageButtonPaginator.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/MessageButtonPaginator.kt index dbf24a97c3..db7cce547b 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/MessageButtonPaginator.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/MessageButtonPaginator.kt @@ -40,89 +40,89 @@ public class MessageButtonPaginator( public val targetChannel: MessageChannelBehavior? = null, public val targetMessage: Message? = null, ) : BaseButtonPaginator(pages, chunkedPages, owner, timeoutSeconds, keepEmbed, switchEmoji, mutator, bundle, locale) { - init { - if (targetChannel == null && targetMessage == null) { - throw IllegalArgumentException("Must provide either a target channel or target message") - } - } + init { + if (targetChannel == null && targetMessage == null) { + throw IllegalArgumentException("Must provide either a target channel or target message") + } + } - /** Specific channel to send the paginator to. **/ - public val channel: MessageChannelBehavior = targetMessage?.channel ?: targetChannel!! + /** Specific channel to send the paginator to. **/ + public val channel: MessageChannelBehavior = targetMessage?.channel ?: targetChannel!! - /** Message containing the paginator. **/ - public var message: Message? = null + /** Message containing the paginator. **/ + public var message: Message? = null - override suspend fun send() { - if (message == null) { - setup() + override suspend fun send() { + if (message == null) { + setup() - message = channel.createMessage { - this.messageReference = targetMessage?.id + message = channel.createMessage { + this.messageReference = targetMessage?.id - allowedMentions { repliedUser = pingInReply } + allowedMentions { repliedUser = pingInReply } applyPage() - with(this@MessageButtonPaginator.components) { - this@createMessage.applyToMessage() - } - } - } else { - updateButtons() + with(this@MessageButtonPaginator.components) { + this@createMessage.applyToMessage() + } + } + } else { + updateButtons() - message!!.edit { + message!!.edit { applyPage() - with(this@MessageButtonPaginator.components) { - this@edit.applyToMessage() - } - } - } - } - - override suspend fun destroy() { - if (!active) { - return - } - - active = false - - if (!keepEmbed) { - message!!.delete() - } else { - message!!.edit { - allowedMentions { repliedUser = pingInReply } + with(this@MessageButtonPaginator.components) { + this@edit.applyToMessage() + } + } + } + } + + override suspend fun destroy() { + if (!active) { + return + } + + active = false + + if (!keepEmbed) { + message!!.delete() + } else { + message!!.edit { + allowedMentions { repliedUser = pingInReply } applyPage() - this.components = mutableListOf() - } - } + this.components = mutableListOf() + } + } - super.destroy() - } + super.destroy() + } } /** Convenience function for creating a message button paginator from a paginator builder. **/ @Suppress("FunctionNaming") // Factory function public fun MessageButtonPaginator( - pingInReply: Boolean = true, - targetChannel: MessageChannelBehavior? = null, - targetMessage: Message? = null, + pingInReply: Boolean = true, + targetChannel: MessageChannelBehavior? = null, + targetMessage: Message? = null, - builder: PaginatorBuilder + builder: PaginatorBuilder, ): MessageButtonPaginator = - MessageButtonPaginator( - pages = builder.pages, + MessageButtonPaginator( + pages = builder.pages, chunkedPages = builder.chunkedPages, - owner = builder.owner, - timeoutSeconds = builder.timeoutSeconds, - keepEmbed = builder.keepEmbed, + owner = builder.owner, + timeoutSeconds = builder.timeoutSeconds, + keepEmbed = builder.keepEmbed, mutator = builder.mutator, - bundle = builder.bundle, - locale = builder.locale, + bundle = builder.bundle, + locale = builder.locale, - pingInReply = pingInReply, - targetChannel = targetChannel, - targetMessage = targetMessage, + pingInReply = pingInReply, + targetChannel = targetChannel, + targetMessage = targetMessage, - switchEmoji = builder.switchEmoji ?: if (builder.pages.groups.size == 2) EXPAND_EMOJI else SWITCH_EMOJI, - ) + switchEmoji = builder.switchEmoji ?: if (builder.pages.groups.size == 2) EXPAND_EMOJI else SWITCH_EMOJI, + ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/PublicFollowUpPaginator.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/PublicFollowUpPaginator.kt index d8a9f0452e..fcceff142b 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/PublicFollowUpPaginator.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/PublicFollowUpPaginator.kt @@ -36,69 +36,69 @@ public class PublicFollowUpPaginator( public val interaction: FollowupPermittingInteractionResponseBehavior, ) : BaseButtonPaginator(pages, chunkedPages, owner, timeoutSeconds, keepEmbed, switchEmoji, mutator, bundle, locale) { - /** Follow-up interaction to use for this paginator's embeds. Will be created by [send]. **/ - public var embedInteraction: PublicFollowupMessage? = null + /** Follow-up interaction to use for this paginator's embeds. Will be created by [send]. **/ + public var embedInteraction: PublicFollowupMessage? = null - override suspend fun send() { - if (embedInteraction == null) { - setup() + override suspend fun send() { + if (embedInteraction == null) { + setup() - embedInteraction = interaction.createPublicFollowup { + embedInteraction = interaction.createPublicFollowup { applyPage() - with(this@PublicFollowUpPaginator.components) { - this@createPublicFollowup.applyToMessage() - } - } - } else { - updateButtons() + with(this@PublicFollowUpPaginator.components) { + this@createPublicFollowup.applyToMessage() + } + } + } else { + updateButtons() - embedInteraction!!.edit { + embedInteraction!!.edit { applyPage() - with(this@PublicFollowUpPaginator.components) { - this@edit.applyToMessage() - } - } - } - } + with(this@PublicFollowUpPaginator.components) { + this@edit.applyToMessage() + } + } + } + } - override suspend fun destroy() { - if (!active) { - return - } + override suspend fun destroy() { + if (!active) { + return + } - active = false + active = false - if (!keepEmbed) { - embedInteraction?.delete() - } else { - embedInteraction?.edit { + if (!keepEmbed) { + embedInteraction?.delete() + } else { + embedInteraction?.edit { applyPage() - this.components = mutableListOf() - } - } + this.components = mutableListOf() + } + } - super.destroy() - } + super.destroy() + } } /** Convenience function for creating an interaction button paginator from a paginator builder. **/ @Suppress("FunctionNaming") // Factory function public fun PublicFollowUpPaginator( - builder: PaginatorBuilder, - interaction: FollowupPermittingInteractionResponseBehavior + builder: PaginatorBuilder, + interaction: FollowupPermittingInteractionResponseBehavior, ): PublicFollowUpPaginator = PublicFollowUpPaginator( - pages = builder.pages, + pages = builder.pages, chunkedPages = builder.chunkedPages, - owner = builder.owner, - timeoutSeconds = builder.timeoutSeconds, - keepEmbed = builder.keepEmbed, + owner = builder.owner, + timeoutSeconds = builder.timeoutSeconds, + keepEmbed = builder.keepEmbed, mutator = builder.mutator, - bundle = builder.bundle, - locale = builder.locale, - interaction = interaction, + bundle = builder.bundle, + locale = builder.locale, + interaction = interaction, - switchEmoji = builder.switchEmoji ?: if (builder.pages.groups.size == 2) EXPAND_EMOJI else SWITCH_EMOJI, + switchEmoji = builder.switchEmoji ?: if (builder.pages.groups.size == 2) EXPAND_EMOJI else SWITCH_EMOJI, ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/PublicResponsePaginator.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/PublicResponsePaginator.kt index fb931737a7..687e23b85e 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/PublicResponsePaginator.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/PublicResponsePaginator.kt @@ -33,59 +33,59 @@ public class PublicResponsePaginator( public val interaction: PublicMessageInteractionResponseBehavior, ) : BaseButtonPaginator(pages, chunkedPages, owner, timeoutSeconds, keepEmbed, switchEmoji, mutator, bundle, locale) { - /** Whether this paginator has been set up for the first time. **/ - public var isSetup: Boolean = false + /** Whether this paginator has been set up for the first time. **/ + public var isSetup: Boolean = false - override suspend fun send() { - if (!isSetup) { - isSetup = true + override suspend fun send() { + if (!isSetup) { + isSetup = true - setup() - } else { - updateButtons() - } + setup() + } else { + updateButtons() + } - interaction.edit { + interaction.edit { applyPage() - with(this@PublicResponsePaginator.components) { - this@edit.applyToMessage() - } - } - } + with(this@PublicResponsePaginator.components) { + this@edit.applyToMessage() + } + } + } - override suspend fun destroy() { - if (!active) { - return - } + override suspend fun destroy() { + if (!active) { + return + } - active = false + active = false - interaction.edit { + interaction.edit { applyPage() - this.components = mutableListOf() - } + this.components = mutableListOf() + } - super.destroy() - } + super.destroy() + } } /** Convenience function for creating an interaction button paginator from a paginator builder. **/ @Suppress("FunctionNaming") // Factory function public fun PublicResponsePaginator( - builder: PaginatorBuilder, - interaction: PublicMessageInteractionResponseBehavior + builder: PaginatorBuilder, + interaction: PublicMessageInteractionResponseBehavior, ): PublicResponsePaginator = PublicResponsePaginator( - pages = builder.pages, + pages = builder.pages, chunkedPages = builder.chunkedPages, - owner = builder.owner, - timeoutSeconds = builder.timeoutSeconds, - keepEmbed = builder.keepEmbed, + owner = builder.owner, + timeoutSeconds = builder.timeoutSeconds, + keepEmbed = builder.keepEmbed, mutator = builder.mutator, - bundle = builder.bundle, - locale = builder.locale, - interaction = interaction, + bundle = builder.bundle, + locale = builder.locale, + interaction = interaction, - switchEmoji = builder.switchEmoji ?: if (builder.pages.groups.size == 2) EXPAND_EMOJI else SWITCH_EMOJI, + switchEmoji = builder.switchEmoji ?: if (builder.pages.groups.size == 2) EXPAND_EMOJI else SWITCH_EMOJI, ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/_Functions.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/_Functions.kt index 0e0a40c45a..0460c1d7be 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/_Functions.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/_Functions.kt @@ -14,28 +14,28 @@ import java.util.* /** Create a paginator that edits the original interaction. **/ public inline fun PublicMessageInteractionResponseBehavior.editingPaginator( - defaultGroup: String = "", - locale: Locale? = null, - builder: (PaginatorBuilder).() -> Unit + defaultGroup: String = "", + locale: Locale? = null, + builder: (PaginatorBuilder).() -> Unit, ): PublicResponsePaginator { - val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) + val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) - builder(pages) + builder(pages) - return PublicResponsePaginator(pages, this) + return PublicResponsePaginator(pages, this) } /** Create a paginator that creates a follow-up message, and edits that. **/ public inline fun FollowupPermittingInteractionResponseBehavior.respondingPaginator( - defaultGroup: String = "", - locale: Locale? = null, - builder: (PaginatorBuilder).() -> Unit + defaultGroup: String = "", + locale: Locale? = null, + builder: (PaginatorBuilder).() -> Unit, ): PublicFollowUpPaginator { - val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) + val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) - builder(pages) + builder(pages) - return PublicFollowUpPaginator(pages, this) + return PublicFollowUpPaginator(pages, this) } /** @@ -43,13 +43,13 @@ public inline fun FollowupPermittingInteractionResponseBehavior.respondingPagina * it's impossible to edit an ephemeral follow-up. */ public inline fun EphemeralMessageInteractionResponseBehavior.editingPaginator( - defaultGroup: String = "", - locale: Locale? = null, - builder: (PaginatorBuilder).() -> Unit + defaultGroup: String = "", + locale: Locale? = null, + builder: (PaginatorBuilder).() -> Unit, ): EphemeralResponsePaginator { - val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) + val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) - builder(pages) + builder(pages) - return EphemeralResponsePaginator(pages, this) + return EphemeralResponsePaginator(pages, this) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/builders/PaginatorBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/builders/PaginatorBuilder.kt index ba660ab657..83cf802ce0 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/builders/PaginatorBuilder.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/builders/PaginatorBuilder.kt @@ -20,53 +20,53 @@ import java.util.* * @param defaultGroup Default page group, if any */ public class PaginatorBuilder( - public var locale: Locale? = null, - public val defaultGroup: String = "" + public var locale: Locale? = null, + public val defaultGroup: String = "", ) { - /** Pages container object. **/ - public val pages: Pages = Pages(defaultGroup) + /** Pages container object. **/ + public val pages: Pages = Pages(defaultGroup) /** How many "pages" should be displayed at once, from 1 to 9. **/ public var chunkedPages: Int = 1 - /** Paginator owner, if only one person should be able to interact. **/ - public var owner: UserBehavior? = null + /** Paginator owner, if only one person should be able to interact. **/ + public var owner: UserBehavior? = null - /** Paginator timeout, in seconds. When elapsed, the paginator will be destroyed. **/ - public var timeoutSeconds: Long? = null + /** Paginator timeout, in seconds. When elapsed, the paginator will be destroyed. **/ + public var timeoutSeconds: Long? = null - /** Whether to keep the paginator content on Discord when the paginator is destroyed. **/ - public var keepEmbed: Boolean = true + /** Whether to keep the paginator content on Discord when the paginator is destroyed. **/ + public var keepEmbed: Boolean = true - /** Alternative switch button emoji, if needed. **/ - public var switchEmoji: ReactionEmoji? = null + /** Alternative switch button emoji, if needed. **/ + public var switchEmoji: ReactionEmoji? = null - /** Translations bundle to use for page groups, if any. **/ - public var bundle: String? = null + /** Translations bundle to use for page groups, if any. **/ + public var bundle: String? = null /** Object containing paginator mutation functions. **/ public var mutator: PageTransitionCallback? = null - /** Add a page to [pages], using the default group. **/ - public fun page(page: Page): Unit = pages.addPage(page) + /** Add a page to [pages], using the default group. **/ + public fun page(page: Page): Unit = pages.addPage(page) - /** Add a page to [pages], using the given group. **/ - public fun page(group: String, page: Page): Unit = pages.addPage(group, page) + /** Add a page to [pages], using the given group. **/ + public fun page(group: String, page: Page): Unit = pages.addPage(group, page) - /** Add a page to [pages], using the default group. **/ - public fun page( - bundle: String? = null, - builder: suspend EmbedBuilder.() -> Unit - ): Unit = - page(Page(builder = builder, bundle = bundle)) + /** Add a page to [pages], using the default group. **/ + public fun page( + bundle: String? = null, + builder: suspend EmbedBuilder.() -> Unit, + ): Unit = + page(Page(builder = builder, bundle = bundle)) - /** Add a page to [pages], using the given group. **/ - public fun page( - group: String, - bundle: String? = null, - builder: suspend EmbedBuilder.() -> Unit - ): Unit = - page(group, Page(builder = builder, bundle = bundle)) + /** Add a page to [pages], using the given group. **/ + public fun page( + group: String, + bundle: String? = null, + builder: suspend EmbedBuilder.() -> Unit, + ): Unit = + page(group, Page(builder = builder, bundle = bundle)) /** * Mutate the paginator and pages, as pages are generated and sent. @@ -74,7 +74,7 @@ public class PaginatorBuilder( * @see PageTransitionCallback */ public suspend fun mutate( - body: suspend PageTransitionCallback.() -> Unit + body: suspend PageTransitionCallback.() -> Unit, ) { val obj = PageTransitionCallback() diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/pages/Page.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/pages/Page.kt index 1a8269c9d1..d92ff59385 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/pages/Page.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/pages/Page.kt @@ -13,7 +13,6 @@ import com.kotlindiscord.kord.extensions.pagination.builders.PageMutator import com.kotlindiscord.kord.extensions.utils.capitalizeWords import com.kotlindiscord.kord.extensions.utils.textOrNull import dev.kord.rest.builder.message.EmbedBuilder -import io.ktor.http.cio.* import org.koin.core.component.inject import java.util.* import kotlin.math.ceil @@ -26,17 +25,17 @@ import kotlin.math.roundToInt * @param builder Embed builder callable for building the page's embed */ public open class Page( - public open val bundle: String? = null, - public open val builder: suspend EmbedBuilder.() -> Unit, + public open val bundle: String? = null, + public open val builder: suspend EmbedBuilder.() -> Unit, ) : KordExKoinComponent { - /** Current instance of the bot. **/ - public open val bot: ExtensibleBot by inject() + /** Current instance of the bot. **/ + public open val bot: ExtensibleBot by inject() - /** Translations provider, for retrieving translations. **/ - public val translationsProvider: TranslationsProvider by inject() + /** Translations provider, for retrieving translations. **/ + public val translationsProvider: TranslationsProvider by inject() - /** Create an embed builder for this page. **/ - public open suspend fun build( + /** Create an embed builder for this page. **/ + public open suspend fun build( locale: Locale, pageNum: Int, chunkSize: Int, @@ -47,8 +46,8 @@ public open class Page( shouldMutateFooter: Boolean = true, shouldPutFooterInDescription: Boolean = false, mutator: PageMutator? = null, - ): suspend EmbedBuilder.() -> Unit = { - builder() + ): suspend EmbedBuilder.() -> Unit = { + builder() if (mutator != null) { mutator(this, this@Page) @@ -125,5 +124,5 @@ public open class Page( } } } - } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/pages/Pages.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/pages/Pages.kt index e161923db0..eb2cf24443 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/pages/Pages.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/pagination/pages/Pages.kt @@ -12,43 +12,43 @@ package com.kotlindiscord.kord.extensions.pagination.pages * @param defaultGroup Default page group, if you have more than one. */ public open class Pages(public open var defaultGroup: String = "") { - /** All groups of pages stored in this class. **/ - public open val groups: LinkedHashMap> = linkedMapOf() + /** All groups of pages stored in this class. **/ + public open val groups: LinkedHashMap> = linkedMapOf() - /** Add a page to the default group. **/ - public open fun addPage(page: Page): Unit = addPage(defaultGroup, page) + /** Add a page to the default group. **/ + public open fun addPage(page: Page): Unit = addPage(defaultGroup, page) - /** Add a page to a given group. **/ - public open fun addPage(group: String, page: Page) { - groups[group] = groups[group] ?: mutableListOf() + /** Add a page to a given group. **/ + public open fun addPage(group: String, page: Page) { + groups[group] = groups[group] ?: mutableListOf() - groups[group]!!.add(page) - } + groups[group]!!.add(page) + } - /** Retrieve the page at the given index, from the default group. **/ - public open fun get(page: Int): Page = get(defaultGroup, page) + /** Retrieve the page at the given index, from the default group. **/ + public open fun get(page: Int): Page = get(defaultGroup, page) - /** Retrieve the page at the given index, from a given group. **/ - public open fun get(group: String, page: Int): Page { - if (groups[group] == null) { - throw NoSuchElementException("No such group: $group") - } + /** Retrieve the page at the given index, from a given group. **/ + public open fun get(group: String, page: Int): Page { + if (groups[group] == null) { + throw NoSuchElementException("No such group: $group") + } - val size = groups[group]!!.size + val size = groups[group]!!.size - if (page > size) { - throw IndexOutOfBoundsException("Page out of range: $page ($size pages)") - } + if (page > size) { + throw IndexOutOfBoundsException("Page out of range: $page ($size pages)") + } - return groups[group]!![page] - } + return groups[group]!![page] + } - /** Check that this Pages object is valid, throwing if it isn't.. **/ - public open fun validate() { - if (groups.isEmpty()) { - throw IllegalArgumentException( - "Invalid pages supplied: At least one page is required" - ) - } - } + /** Check that this Pages object is valid, throwing if it isn't.. **/ + public open fun validate() { + if (groups.isEmpty()) { + throw IllegalArgumentException( + "Invalid pages supplied: At least one page is required" + ) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/BooleanParser.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/BooleanParser.kt index 14eedb1284..f939917cf3 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/BooleanParser.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/BooleanParser.kt @@ -24,44 +24,44 @@ import java.util.* * Translations may be split using commas, in which case any of the given values will be suitable. */ public object BooleanParser : KordExKoinComponent { - private val translations: TranslationsProvider by inject() - private val settings: ExtensibleBotBuilder by inject() + private val translations: TranslationsProvider by inject() + private val settings: ExtensibleBotBuilder by inject() - private val valueCache: MutableMap, List>> = mutableMapOf() + private val valueCache: MutableMap, List>> = mutableMapOf() - /** - * Parse the given string into a [Boolean] based on the translations for the given locale. Falls back to the bot's - * default locale as required. - */ - public fun parse(input: String, locale: Locale): Boolean? { - if (valueCache[locale] == null) { - val trueValues = translations.translate("utils.string.true", locale) - .split(',') - .map { it.trim() } + /** + * Parse the given string into a [Boolean] based on the translations for the given locale. Falls back to the bot's + * default locale as required. + */ + public fun parse(input: String, locale: Locale): Boolean? { + if (valueCache[locale] == null) { + val trueValues = translations.translate("utils.string.true", locale) + .split(',') + .map { it.trim() } - val falseValues = translations.translate("utils.string.false", locale) - .split(',') - .map { it.trim() } + val falseValues = translations.translate("utils.string.false", locale) + .split(',') + .map { it.trim() } - valueCache[locale] = trueValues to falseValues - } + valueCache[locale] = trueValues to falseValues + } - val (trueValues, falseValues) = valueCache[locale]!! - val lowerInput = input.lowercase() + val (trueValues, falseValues) = valueCache[locale]!! + val lowerInput = input.lowercase() - val result = when { - trueValues.contains(lowerInput) -> true - falseValues.contains(lowerInput) -> false + val result = when { + trueValues.contains(lowerInput) -> true + falseValues.contains(lowerInput) -> false - else -> null - } + else -> null + } - if (result == null && locale != settings.i18nBuilder.defaultLocale) { - // Try it again in the default locale as a fallback + if (result == null && locale != settings.i18nBuilder.defaultLocale) { + // Try it again in the default locale as a fallback - return parse(input, settings.i18nBuilder.defaultLocale) - } + return parse(input, settings.i18nBuilder.defaultLocale) + } - return result - } + return result + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/ColorParser.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/ColorParser.kt index 4a9cd60eb7..43c11c6c99 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/ColorParser.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/ColorParser.kt @@ -30,16 +30,16 @@ import java.util.* * Translations may be split using commas, in which case any of the given values will be suitable. */ public object ColorParser : KordExKoinComponent { - private val settings: ExtensibleBotBuilder by inject() + private val settings: ExtensibleBotBuilder by inject() - /** - * Parse the given string into a [Color] based on the translations for the given locale. Falls back to the bot's - * default locale as required. - */ - public fun parse(input: String, locale: Locale): Color? { - val defaultColorMap = ColorCache.getColors(settings.i18nBuilder.defaultLocale) - val colorMap = ColorCache.getColors(locale) + /** + * Parse the given string into a [Color] based on the translations for the given locale. Falls back to the bot's + * default locale as required. + */ + public fun parse(input: String, locale: Locale): Color? { + val defaultColorMap = ColorCache.getColors(settings.i18nBuilder.defaultLocale) + val colorMap = ColorCache.getColors(locale) - return colorMap[input] ?: defaultColorMap[input] - } + return colorMap[input] ?: defaultColorMap[input] + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/DurationParser.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/DurationParser.kt index 9446103101..ca455cb154 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/DurationParser.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/DurationParser.kt @@ -22,65 +22,65 @@ private const val DAYS_IN_WEEK = 7 * Object in charge of parsing strings into [DateTimePeriod]s, using translated locale-aware units. */ public object DurationParser : KordExKoinComponent { - private val translations: TranslationsProvider by inject() - - /** Check whether the given character is a valid duration unit character. **/ - public fun charValid(char: Char, locale: Locale): Boolean = - char.isDigit() || - char == ' ' || - TimeUnitCache.getUnits(locale).filterKeys { it.startsWith(char) }.isNotEmpty() - - /** - * Parse the provided string to a [DateTimePeriod] object, using the strings provided by the given [Locale]. - */ - public fun parse(input: String, locale: Locale): DateTimePeriod { - val unitMap = TimeUnitCache.getUnits(locale) - - val units: MutableList = mutableListOf() - val values: MutableList = mutableListOf() - - var buffer = input.replace(",", "") - .replace("+", "") - .replace(" ", "") - - while (buffer.isNotEmpty()) { - buffer = if (isValueChar(buffer.first())) { - val (value, remaining) = buffer.splitOn(DurationParser::isNotValueChar) - - values.add(value) - remaining - } else { - val (unit, remaining) = buffer.splitOn(DurationParser::isValueChar) - - units.add(unit) - remaining - } - } - - if (values.size != units.size) { - throw DurationParserException(translations.translate("converters.duration.error.badUnitPairs", locale)) - } - - val allValues: MutableStringKeyedMap = mutableMapOf() - - while (units.isNotEmpty()) { - val (unitString, valueString) = units.removeFirst() to values.removeFirst() - val timeUnit = unitMap[unitString.lowercase()] ?: throw InvalidTimeUnitException(unitString) - - allValues[timeUnit.name] = allValues[timeUnit.name] ?: 0 - allValues[timeUnit.name] = allValues[timeUnit.name]!!.plus(valueString.toInt()) - } - - return DateTimePeriod( - years = allValues["years"] ?: 0, - months = allValues["months"] ?: 0, - days = (allValues["days"] ?: 0) + (allValues["weeks"]?.times(DAYS_IN_WEEK) ?: 0), - hours = allValues["hours"] ?: 0, - minutes = allValues["minutes"] ?: 0, - seconds = allValues["seconds"] ?: 0 - ) - } - - private fun isValueChar(char: Char) = char.isDigit() || char == '-' - private fun isNotValueChar(char: Char) = !isValueChar(char) + private val translations: TranslationsProvider by inject() + + /** Check whether the given character is a valid duration unit character. **/ + public fun charValid(char: Char, locale: Locale): Boolean = + char.isDigit() || + char == ' ' || + TimeUnitCache.getUnits(locale).filterKeys { it.startsWith(char) }.isNotEmpty() + + /** + * Parse the provided string to a [DateTimePeriod] object, using the strings provided by the given [Locale]. + */ + public fun parse(input: String, locale: Locale): DateTimePeriod { + val unitMap = TimeUnitCache.getUnits(locale) + + val units: MutableList = mutableListOf() + val values: MutableList = mutableListOf() + + var buffer = input.replace(",", "") + .replace("+", "") + .replace(" ", "") + + while (buffer.isNotEmpty()) { + buffer = if (isValueChar(buffer.first())) { + val (value, remaining) = buffer.splitOn(DurationParser::isNotValueChar) + + values.add(value) + remaining + } else { + val (unit, remaining) = buffer.splitOn(DurationParser::isValueChar) + + units.add(unit) + remaining + } + } + + if (values.size != units.size) { + throw DurationParserException(translations.translate("converters.duration.error.badUnitPairs", locale)) + } + + val allValues: MutableStringKeyedMap = mutableMapOf() + + while (units.isNotEmpty()) { + val (unitString, valueString) = units.removeFirst() to values.removeFirst() + val timeUnit = unitMap[unitString.lowercase()] ?: throw InvalidTimeUnitException(unitString) + + allValues[timeUnit.name] = allValues[timeUnit.name] ?: 0 + allValues[timeUnit.name] = allValues[timeUnit.name]!!.plus(valueString.toInt()) + } + + return DateTimePeriod( + years = allValues["years"] ?: 0, + months = allValues["months"] ?: 0, + days = (allValues["days"] ?: 0) + (allValues["weeks"]?.times(DAYS_IN_WEEK) ?: 0), + hours = allValues["hours"] ?: 0, + minutes = allValues["minutes"] ?: 0, + seconds = allValues["seconds"] ?: 0 + ) + } + + private fun isValueChar(char: Char) = char.isDigit() || char == '-' + private fun isNotValueChar(char: Char) = !isValueChar(char) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/Exceptions.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/Exceptions.kt index 6c8972b942..f9332dcf6b 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/Exceptions.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/Exceptions.kt @@ -19,8 +19,8 @@ public open class BaseParserException : KordExException() * @param error Human-readable error text. */ public open class DurationParserException(public open var error: String) : BaseParserException() { - override val message: String? = error - override fun toString(): String = error + override val message: String? = error + override fun toString(): String = error } /** diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/caches/ColorCache.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/caches/ColorCache.kt index 7377ce51e7..a4c39d7020 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/caches/ColorCache.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/caches/ColorCache.kt @@ -16,36 +16,36 @@ import java.util.* private typealias ColorMap = LinkedHashMap private val keyMap: ColorMap = linkedMapOf( - "utils.colors.black" to DISCORD_BLACK, - "utils.colors.blurple" to DISCORD_BLURPLE, - "utils.colors.fuchsia" to DISCORD_FUCHSIA, - "utils.colors.green" to DISCORD_GREEN, - "utils.colors.red" to DISCORD_RED, - "utils.colors.white" to DISCORD_WHITE, - "utils.colors.yellow" to DISCORD_YELLOW, + "utils.colors.black" to DISCORD_BLACK, + "utils.colors.blurple" to DISCORD_BLURPLE, + "utils.colors.fuchsia" to DISCORD_FUCHSIA, + "utils.colors.green" to DISCORD_GREEN, + "utils.colors.red" to DISCORD_RED, + "utils.colors.white" to DISCORD_WHITE, + "utils.colors.yellow" to DISCORD_YELLOW, ) /** Simple object that caches translated colors per locale. **/ public object ColorCache : KordExKoinComponent { - private val translations: TranslationsProvider by inject() - private val valueCache: MutableMap = mutableMapOf() + private val translations: TranslationsProvider by inject() + private val valueCache: MutableMap = mutableMapOf() - /** Return a mapping of all translated colour names to Color objects, based on the given locale. **/ - public fun getColors(locale: Locale): ColorMap { - if (valueCache[locale] == null) { - val colorMap: ColorMap = linkedMapOf() + /** Return a mapping of all translated colour names to Color objects, based on the given locale. **/ + public fun getColors(locale: Locale): ColorMap { + if (valueCache[locale] == null) { + val colorMap: ColorMap = linkedMapOf() - keyMap.forEach { (key, value) -> - val result = translations.translate(key, locale) + keyMap.forEach { (key, value) -> + val result = translations.translate(key, locale) - result.split(",").map { it.trim() }.forEach { - colorMap[it] = value - } - } + result.split(",").map { it.trim() }.forEach { + colorMap[it] = value + } + } - valueCache[locale] = colorMap - } + valueCache[locale] = colorMap + } - return valueCache[locale]!! - } + return valueCache[locale]!! + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/caches/TimeUnitCache.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/caches/TimeUnitCache.kt index eae7345441..0e86411c5f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/caches/TimeUnitCache.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/parsers/caches/TimeUnitCache.kt @@ -18,35 +18,35 @@ private typealias UnitMap = LinkedHashMap * Simple object that caches translated time units per locale. */ public object TimeUnitCache : KordExKoinComponent { - private val translations: TranslationsProvider by inject() - private val valueCache: MutableMap = mutableMapOf() - - private val keyMap: UnitMap = linkedMapOf( - "utils.units.second" to DateTimeUnit.SECOND, - "utils.units.minute" to DateTimeUnit.MINUTE, - "utils.units.hour" to DateTimeUnit.HOUR, - "utils.units.day" to DateTimeUnit.DAY, - "utils.units.week" to DateTimeUnit.WEEK, - "utils.units.month" to DateTimeUnit.MONTH, - "utils.units.year" to DateTimeUnit.YEAR, - ) - - /** Return a mapping of all translated unit names to DateTimeUnit objects, based on the given locale. **/ - public fun getUnits(locale: Locale): UnitMap { - if (valueCache[locale] == null) { - val unitMap: UnitMap = linkedMapOf() - - keyMap.forEach { (key, value) -> - val result = translations.translate(key, locale) - - result.split(",").map { it.trim() }.forEach { - unitMap[it] = value - } - } - - valueCache[locale] = unitMap - } - - return valueCache[locale]!! - } + private val translations: TranslationsProvider by inject() + private val valueCache: MutableMap = mutableMapOf() + + private val keyMap: UnitMap = linkedMapOf( + "utils.units.second" to DateTimeUnit.SECOND, + "utils.units.minute" to DateTimeUnit.MINUTE, + "utils.units.hour" to DateTimeUnit.HOUR, + "utils.units.day" to DateTimeUnit.DAY, + "utils.units.week" to DateTimeUnit.WEEK, + "utils.units.month" to DateTimeUnit.MONTH, + "utils.units.year" to DateTimeUnit.YEAR, + ) + + /** Return a mapping of all translated unit names to DateTimeUnit objects, based on the given locale. **/ + public fun getUnits(locale: Locale): UnitMap { + if (valueCache[locale] == null) { + val unitMap: UnitMap = linkedMapOf() + + keyMap.forEach { (key, value) -> + val result = translations.translate(key, locale) + + result.split(",").map { it.trim() }.forEach { + unitMap[it] = value + } + } + + valueCache[locale] = unitMap + } + + return valueCache[locale]!! + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/DefaultLoader.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/DefaultLoader.kt index 3c7789c742..27fa06404f 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/DefaultLoader.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/DefaultLoader.kt @@ -12,16 +12,16 @@ import java.nio.file.Path /** Default plugin loader, with a changed classloader. **/ public class DefaultLoader(pluginManager: PluginManager?) : DefaultPluginLoader(pluginManager) { - override fun loadPlugin(pluginPath: Path?, pluginDescriptor: PluginDescriptor?): ClassLoader { - val pluginClassLoader = PluginClassLoader( - pluginManager, - pluginDescriptor, - ClassLoader.getSystemClassLoader(), - ClassLoadingStrategy.APD - ) + override fun loadPlugin(pluginPath: Path?, pluginDescriptor: PluginDescriptor?): ClassLoader { + val pluginClassLoader = PluginClassLoader( + pluginManager, + pluginDescriptor, + ClassLoader.getSystemClassLoader(), + ClassLoadingStrategy.APD + ) - pluginClassLoader.addFile(pluginPath!!.toFile()) + pluginClassLoader.addFile(pluginPath!!.toFile()) - return pluginClassLoader - } + return pluginClassLoader + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/DevelopmentLoader.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/DevelopmentLoader.kt index a833612c3d..8f4dbafb3a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/DevelopmentLoader.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/DevelopmentLoader.kt @@ -12,16 +12,16 @@ import java.nio.file.Path /** Development plugin loader, with a changed classloader. **/ public class DevelopmentLoader(pluginManager: PluginManager?) : DevelopmentPluginLoader(pluginManager) { - override fun loadPlugin(pluginPath: Path?, pluginDescriptor: PluginDescriptor?): ClassLoader { - val pluginClassLoader = PluginClassLoader( - pluginManager, - pluginDescriptor, - ClassLoader.getSystemClassLoader(), - ClassLoadingStrategy.APD - ) + override fun loadPlugin(pluginPath: Path?, pluginDescriptor: PluginDescriptor?): ClassLoader { + val pluginClassLoader = PluginClassLoader( + pluginManager, + pluginDescriptor, + ClassLoader.getSystemClassLoader(), + ClassLoadingStrategy.APD + ) - pluginClassLoader.addFile(pluginPath!!.toFile()) + pluginClassLoader.addFile(pluginPath!!.toFile()) - return pluginClassLoader - } + return pluginClassLoader + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/JarLoader.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/JarLoader.kt index 597d82b565..bc00a91e39 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/JarLoader.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/JarLoader.kt @@ -12,16 +12,16 @@ import java.nio.file.Path /** JAR plugin loader, with a changed classloader. **/ public class JarLoader(pluginManager: PluginManager?) : JarPluginLoader(pluginManager) { - override fun loadPlugin(pluginPath: Path?, pluginDescriptor: PluginDescriptor?): ClassLoader { - val pluginClassLoader = PluginClassLoader( - pluginManager, - pluginDescriptor, - ClassLoader.getSystemClassLoader(), - ClassLoadingStrategy.APD - ) + override fun loadPlugin(pluginPath: Path?, pluginDescriptor: PluginDescriptor?): ClassLoader { + val pluginClassLoader = PluginClassLoader( + pluginManager, + pluginDescriptor, + ClassLoader.getSystemClassLoader(), + ClassLoadingStrategy.APD + ) - pluginClassLoader.addFile(pluginPath!!.toFile()) + pluginClassLoader.addFile(pluginPath!!.toFile()) - return pluginClassLoader - } + return pluginClassLoader + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/KordExPlugin.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/KordExPlugin.kt index f1b1fec4d4..4805a03853 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/KordExPlugin.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/KordExPlugin.kt @@ -23,72 +23,72 @@ import org.pf4j.PluginWrapper * and ready to be used. */ public abstract class KordExPlugin(wrapper: PluginWrapper) : Plugin(wrapper), KordExKoinComponent { - internal val extensions: MutableStringKeyedMap = mutableMapOf() - internal val settingsCallbacks: MutableList = mutableListOf() - - /** Quick access to the bot object. **/ - public val bot: ExtensibleBot by inject() - - /** Quick access to the Kord object. **/ - public val kord: Kord by inject() - - /** - * Override this to set up and configure your plugin. - * - * You'll want to call [extension] to register any extensions here, so they can be loaded and unloaded with - * the plugin. - */ - public abstract suspend fun setup() - - /** - * Register an extension to be loaded that's part of this plugin. - * - * You'll probably just want to pass the constructor for your extension. However, **do note that the builder - * will be called immediately,** in order to resolve its name. It will be called again later when the bot - * loads up your extensions. - */ - public open fun extension(builder: ExtensionBuilder) { - val extension = builder() - - extensions[extension.name] = builder - } - - /** - * Modify the current bot's settings. - * - * **Do not register extensions here, use [extension]!** - */ - public open fun settings(body: SettingsCallback) { - settingsCallbacks.add(body) - } - - internal suspend fun asyncStart() { - setup() - - extensions.values.forEach { bot.addExtension(it) } - } - - internal suspend fun asyncStop() { - extensions.keys.forEach { - bot.unloadExtension(it) - } - } - - internal suspend fun asyncDelete() { - extensions.keys.forEach { - bot.removeExtension(it) - } - } - - override fun start(): Unit = runBlocking { - kord.launch { asyncStart() }.join() - } - - override fun stop(): Unit = runBlocking { - kord.launch { asyncStop() }.join() - } - - override fun delete(): Unit = runBlocking { - kord.launch { asyncDelete() }.join() - } + internal val extensions: MutableStringKeyedMap = mutableMapOf() + internal val settingsCallbacks: MutableList = mutableListOf() + + /** Quick access to the bot object. **/ + public val bot: ExtensibleBot by inject() + + /** Quick access to the Kord object. **/ + public val kord: Kord by inject() + + /** + * Override this to set up and configure your plugin. + * + * You'll want to call [extension] to register any extensions here, so they can be loaded and unloaded with + * the plugin. + */ + public abstract suspend fun setup() + + /** + * Register an extension to be loaded that's part of this plugin. + * + * You'll probably just want to pass the constructor for your extension. However, **do note that the builder + * will be called immediately,** in order to resolve its name. It will be called again later when the bot + * loads up your extensions. + */ + public open fun extension(builder: ExtensionBuilder) { + val extension = builder() + + extensions[extension.name] = builder + } + + /** + * Modify the current bot's settings. + * + * **Do not register extensions here, use [extension]!** + */ + public open fun settings(body: SettingsCallback) { + settingsCallbacks.add(body) + } + + internal suspend fun asyncStart() { + setup() + + extensions.values.forEach { bot.addExtension(it) } + } + + internal suspend fun asyncStop() { + extensions.keys.forEach { + bot.unloadExtension(it) + } + } + + internal suspend fun asyncDelete() { + extensions.keys.forEach { + bot.removeExtension(it) + } + } + + override fun start(): Unit = runBlocking { + kord.launch { asyncStart() }.join() + } + + override fun stop(): Unit = runBlocking { + kord.launch { asyncStop() }.join() + } + + override fun delete(): Unit = runBlocking { + kord.launch { asyncDelete() }.join() + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginManager.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginManager.kt index da4f12b878..777f980531 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginManager.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginManager.kt @@ -12,13 +12,13 @@ import java.nio.file.Path @Suppress("SpreadOperator") /** Module manager, in charge of loading and managing module "plugins". **/ public open class PluginManager(roots: List) : JarPluginManager(*roots.toTypedArray()) { - override fun createPluginDescriptorFinder(): PluginDescriptorFinder = - PropertiesPluginDescriptorFinder() + override fun createPluginDescriptorFinder(): PluginDescriptorFinder = + PropertiesPluginDescriptorFinder() - override fun createPluginLoader(): PluginLoader? { - return CompoundPluginLoader() - .add(DevelopmentLoader(this)) { this.isDevelopment } - .add(JarLoader(this)) { this.isNotDevelopment } - .add(DefaultLoader(this)) { this.isNotDevelopment } - } + override fun createPluginLoader(): PluginLoader? { + return CompoundPluginLoader() + .add(DevelopmentLoader(this)) { this.isDevelopment } + .add(JarLoader(this)) { this.isNotDevelopment } + .add(DefaultLoader(this)) { this.isNotDevelopment } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/AbstractDeconstructingApplicationCommandRegistryStorage.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/AbstractDeconstructingApplicationCommandRegistryStorage.kt index bb0a04eea9..fbb663d331 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/AbstractDeconstructingApplicationCommandRegistryStorage.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/AbstractDeconstructingApplicationCommandRegistryStorage.kt @@ -19,69 +19,69 @@ import kotlinx.coroutines.flow.mapNotNull * For simplicity the parameter / return types of the abstract methods are all [String]s. */ public abstract class AbstractDeconstructingApplicationCommandRegistryStorage> : - RegistryStorage { + RegistryStorage { - /** - * Mapping of command-key to command-object. - */ - protected open val commandMapping: MutableStringKeyedMap = mutableMapOf() + /** + * Mapping of command-key to command-object. + */ + protected open val commandMapping: MutableStringKeyedMap = mutableMapOf() - /** - * Upserts simplified data. - * The key is the command id, which is returned by the create request from discord. - * The value is the command name, which must be unique across the registry. - */ - protected abstract suspend fun upsert(key: String, value: String) + /** + * Upserts simplified data. + * The key is the command id, which is returned by the create request from discord. + * The value is the command name, which must be unique across the registry. + */ + protected abstract suspend fun upsert(key: String, value: String) - /** - * Reads simplified data from the storage. - * - * The key is the command id. - * - * Returns the command name associated with this key. - */ - protected abstract suspend fun read(key: String): String? + /** + * Reads simplified data from the storage. + * + * The key is the command id. + * + * Returns the command name associated with this key. + */ + protected abstract suspend fun read(key: String): String? - /** - * Deletes and returns simplified data. - * - * The key is the command id. - * - * Returns the command name associated with this key. - */ - protected abstract suspend fun delete(key: String): String? + /** + * Deletes and returns simplified data. + * + * The key is the command id. + * + * Returns the command name associated with this key. + */ + protected abstract suspend fun delete(key: String): String? - /** - * Returns all entries in this registry as simplified data. - * - * The key is the command id. - * The value is the command name associated with this key. - */ - protected abstract fun entries(): Flow> + /** + * Returns all entries in this registry as simplified data. + * + * The key is the command id. + * The value is the command name associated with this key. + */ + protected abstract fun entries(): Flow> - override fun constructUniqueIdentifier(data: T): String = - "${data.name}-${data.type.value}-${data.guildId?.value ?: 0}" + override fun constructUniqueIdentifier(data: T): String = + "${data.name}-${data.type.value}-${data.guildId?.value ?: 0}" - override suspend fun register(data: T) { - commandMapping[constructUniqueIdentifier(data)] = data - } + override suspend fun register(data: T) { + commandMapping[constructUniqueIdentifier(data)] = data + } - override suspend fun set(id: Snowflake, data: T) { - val key = constructUniqueIdentifier(data) - commandMapping[key] = data - upsert(id.toString(), key) - } + override suspend fun set(id: Snowflake, data: T) { + val key = constructUniqueIdentifier(data) + commandMapping[key] = data + upsert(id.toString(), key) + } - override suspend fun get(id: Snowflake): T? { - val key = read(id.toString()) ?: return null - return commandMapping[key] - } + override suspend fun get(id: Snowflake): T? { + val key = read(id.toString()) ?: return null + return commandMapping[key] + } - override suspend fun remove(id: Snowflake): T? { - val key = delete(id.toString()) ?: return null - return commandMapping[key] - } + override suspend fun remove(id: Snowflake): T? { + val key = delete(id.toString()) ?: return null + return commandMapping[key] + } - override fun entryFlow(): Flow> = entries() - .mapNotNull { commandMapping[it.value]?.let { cmd -> RegistryStorage.StorageEntry(Snowflake(it.key), cmd) } } + override fun entryFlow(): Flow> = entries() + .mapNotNull { commandMapping[it.value]?.let { cmd -> RegistryStorage.StorageEntry(Snowflake(it.key), cmd) } } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/DefaultLocalRegistryStorage.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/DefaultLocalRegistryStorage.kt index c0182855ec..2b42c6a0db 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/DefaultLocalRegistryStorage.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/DefaultLocalRegistryStorage.kt @@ -16,25 +16,25 @@ import kotlinx.coroutines.flow.map */ public open class DefaultLocalRegistryStorage : RegistryStorage { - protected open val registry: MutableMap = mutableMapOf() + protected open val registry: MutableMap = mutableMapOf() - override suspend fun register(data: T) { - // we don't need to do anything here - } + override suspend fun register(data: T) { + // we don't need to do anything here + } - override suspend fun set(id: K, data: T) { - registry[id] = data - } + override suspend fun set(id: K, data: T) { + registry[id] = data + } - override suspend fun get(id: K): T? = registry[id] + override suspend fun get(id: K): T? = registry[id] - override suspend fun remove(id: K): T? = registry.remove(id) + override suspend fun remove(id: K): T? = registry.remove(id) - override fun entryFlow(): Flow> { - return registry.entries - .asFlow() - .map { RegistryStorage.StorageEntry(it.key, it.value) } - } + override fun entryFlow(): Flow> { + return registry.entries + .asFlow() + .map { RegistryStorage.StorageEntry(it.key, it.value) } + } - override fun constructUniqueIdentifier(data: T): String = data.hashCode().toString() + override fun constructUniqueIdentifier(data: T): String = data.hashCode().toString() } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/RegistryStorage.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/RegistryStorage.kt index ba8165ac42..ac305ced70 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/RegistryStorage.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/registry/RegistryStorage.kt @@ -16,56 +16,56 @@ import kotlinx.coroutines.flow.Flow */ public interface RegistryStorage { - /** - * Lets the registry know about the specified type [T], this may store the object in a local map, - * which is used for reconstructing later. - */ - public suspend fun register(data: T) + /** + * Lets the registry know about the specified type [T], this may store the object in a local map, + * which is used for reconstructing later. + */ + public suspend fun register(data: T) - /** - * Creates or updates an existing entry at the given unique key. - * - * This may deconstruct the given data and only persists a partial object. - */ - public suspend fun set(id: K, data: T) + /** + * Creates or updates an existing entry at the given unique key. + * + * This may deconstruct the given data and only persists a partial object. + */ + public suspend fun set(id: K, data: T) - /** - * Reads a value from the registry at the given key. - * - * This may reconstruct the data from a partial object. - */ - public suspend fun get(id: K): T? + /** + * Reads a value from the registry at the given key. + * + * This may reconstruct the data from a partial object. + */ + public suspend fun get(id: K): T? - /** - * Deletes a value from the registry with the given key. - * - * The return value may be a reconstructed object from partial data. - */ - public suspend fun remove(id: K): T? + /** + * Deletes a value from the registry with the given key. + * + * The return value may be a reconstructed object from partial data. + */ + public suspend fun remove(id: K): T? - /** - * Creates a flow of all entries in this registry. - * - * The objects in this flow may be reconstructed from partial data. - */ - public fun entryFlow(): Flow> + /** + * Creates a flow of all entries in this registry. + * + * The objects in this flow may be reconstructed from partial data. + */ + public fun entryFlow(): Flow> - /** - * Constructs a unique key for the given data. - */ - public fun constructUniqueIdentifier(data: T): String + /** + * Constructs a unique key for the given data. + */ + public fun constructUniqueIdentifier(data: T): String - /** - * Data class to represent an entry in the [RegistryStorage]. - */ - public data class StorageEntry( - /** - * The key of this entry. - */ - val key: K, - /** - * The value of this entry. - */ - val value: V - ) + /** + * Data class to represent an entry in the [RegistryStorage]. + */ + public data class StorageEntry( + /** + * The key of this entry. + */ + val key: K, + /** + * The value of this entry. + */ + val value: V, + ) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/BreadcrumbType.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/BreadcrumbType.kt index d6060d0fe7..6a668cc862 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/BreadcrumbType.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/BreadcrumbType.kt @@ -13,33 +13,33 @@ package com.kotlindiscord.kord.extensions.sentry * @param requiredKeys Array of required keys that must be present in the breadcrumb data for it to be valid, if any */ public sealed class BreadcrumbType(public val name: String, public vararg val requiredKeys: String) { - /** Typically a debug log message. **/ - public object Debug : BreadcrumbType("debug") + /** Typically a debug log message. **/ + public object Debug : BreadcrumbType("debug") - /** The default breadcrumb type. **/ - public object Default : BreadcrumbType("default") + /** The default breadcrumb type. **/ + public object Default : BreadcrumbType("default") - /** A detected or unhandled error. **/ - public object Error : BreadcrumbType("error") + /** A detected or unhandled error. **/ + public object Error : BreadcrumbType("error") - /** An HTTP request sent by your bot. **/ - public object HTTP : BreadcrumbType("http") + /** An HTTP request sent by your bot. **/ + public object HTTP : BreadcrumbType("http") - /** Information on what's been going on. **/ - public object Info : BreadcrumbType("info") + /** Information on what's been going on. **/ + public object Info : BreadcrumbType("info") - /** Navigation action, requiring from/to data keys. **/ - public object Navigation : BreadcrumbType("navigation", "from", "to") + /** Navigation action, requiring from/to data keys. **/ + public object Navigation : BreadcrumbType("navigation", "from", "to") - /** A query made by a user. **/ - public object Query : BreadcrumbType("query") + /** A query made by a user. **/ + public object Query : BreadcrumbType("query") - /** A tracing event. **/ - public object Transaction : BreadcrumbType("transaction") + /** A tracing event. **/ + public object Transaction : BreadcrumbType("transaction") - /** A UI interaction. **/ - public object UI : BreadcrumbType("ui") + /** A UI interaction. **/ + public object UI : BreadcrumbType("ui") - /** A user interaction. **/ - public object User : BreadcrumbType("user") + /** A user interaction. **/ + public object User : BreadcrumbType("user") } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/SentryIdConverter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/SentryIdConverter.kt index fb31cb553f..379e5b1c04 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/SentryIdConverter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/SentryIdConverter.kt @@ -32,39 +32,39 @@ import io.sentry.protocol.SentryId types = [ConverterType.SINGLE, ConverterType.LIST, ConverterType.OPTIONAL] ) public class SentryIdConverter( - override var validator: Validator = null + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "extensions.sentry.converter.sentryId.signatureType" + override val signatureTypeString: String = "extensions.sentry.converter.sentryId.signatureType" override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - try { - this.parsed = SentryId(arg) - } catch (e: IllegalArgumentException) { - throw DiscordRelayedException( - context.translate("extensions.sentry.converter.error.invalid", replacements = arrayOf(arg)) - ) - } + try { + this.parsed = SentryId(arg) + } catch (e: IllegalArgumentException) { + throw DiscordRelayedException( + context.translate("extensions.sentry.converter.error.invalid", replacements = arrayOf(arg)) + ) + } - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val optionValue = (option as? StringOptionValue)?.value ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val optionValue = (option as? StringOptionValue)?.value ?: return false - try { - this.parsed = SentryId(optionValue) - } catch (e: IllegalArgumentException) { - throw DiscordRelayedException( - context.translate("extensions.sentry.converter.error.invalid", replacements = arrayOf(optionValue)) - ) - } + try { + this.parsed = SentryId(optionValue) + } catch (e: IllegalArgumentException) { + throw DiscordRelayedException( + context.translate("extensions.sentry.converter.error.invalid", replacements = arrayOf(optionValue)) + ) + } - return true - } + return true + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/captures/SentryExceptionCapture.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/captures/SentryExceptionCapture.kt index 85295d7000..3df7477392 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/captures/SentryExceptionCapture.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/sentry/captures/SentryExceptionCapture.kt @@ -11,7 +11,7 @@ import org.jetbrains.annotations.ApiStatus * @param throwable The [Throwable] to submit to Sentry. */ public class SentryExceptionCapture( - public val throwable: Throwable + public val throwable: Throwable, ) : SentryScopeCapture() { /** @suppress Function meant for internal use. **/ @ApiStatus.Internal diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/DataAdapter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/DataAdapter.kt index 4fc05c1497..482737c117 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/DataAdapter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/DataAdapter.kt @@ -29,92 +29,92 @@ package com.kotlindiscord.kord.extensions.storage * File-based data adapters will likely just use the file path here. */ public abstract class DataAdapter { - /** A simple map that maps data IDs to data objects. **/ - protected open val dataCache: MutableMap = mutableMapOf() + /** A simple map that maps data IDs to data objects. **/ + protected open val dataCache: MutableMap = mutableMapOf() - /** A simple map that maps storage units to your data IDs. **/ - protected open val unitCache: MutableMap, ID> = mutableMapOf() + /** A simple map that maps storage units to your data IDs. **/ + protected open val unitCache: MutableMap, ID> = mutableMapOf() - /** - * Entirely removes the data represented by the given storage unit from the [dataCache], the disk if this is an - * adapter that stores data in files, or from whatever relevant persistent storage is being used. - * - * @return Whether the data was deleted - should return `false` if it didn't exist. - */ - public abstract suspend fun delete(unit: StorageUnit): Boolean + /** + * Entirely removes the data represented by the given storage unit from the [dataCache], the disk if this is an + * adapter that stores data in files, or from whatever relevant persistent storage is being used. + * + * @return Whether the data was deleted - should return `false` if it didn't exist. + */ + public abstract suspend fun delete(unit: StorageUnit): Boolean - /** - * Retrieve and return the data object represented by the given storage unit. - * - * This function should attempt to retrieve from the [dataCache] first, and return the result of [reload] if it - * isn't present there. - * - * @return The loaded data if it was found, `null` otherwise. - */ - public abstract suspend fun get(unit: StorageUnit): R? + /** + * Retrieve and return the data object represented by the given storage unit. + * + * This function should attempt to retrieve from the [dataCache] first, and return the result of [reload] if it + * isn't present there. + * + * @return The loaded data if it was found, `null` otherwise. + */ + public abstract suspend fun get(unit: StorageUnit): R? - /** - * Retrieve the data object represented by the given storage unit, or store the data object returned by the - * callback if no respective data could be found. - * - * This is similar to the `getOrDefault` you'd find on collections, but it also saves the default for you. You - * can use an elvis operator (`?:`) if you don't want to save. - * - * This function takes a lambda so that data objects aren't created unless they're needed. - * - * @return The stored data, or the data you passed if there was nothing stored. - */ - public open suspend fun getOrSaveDefault(unit: StorageUnit, data: suspend () -> R): R = - get(unit) ?: save(unit, data()) + /** + * Retrieve the data object represented by the given storage unit, or store the data object returned by the + * callback if no respective data could be found. + * + * This is similar to the `getOrDefault` you'd find on collections, but it also saves the default for you. You + * can use an elvis operator (`?:`) if you don't want to save. + * + * This function takes a lambda so that data objects aren't created unless they're needed. + * + * @return The stored data, or the data you passed if there was nothing stored. + */ + public open suspend fun getOrSaveDefault(unit: StorageUnit, data: suspend () -> R): R = + get(unit) ?: save(unit, data()) - /** - * Retrieve the data represented by the given storage unit from persistent storage, storing it in the [dataCache] - * and returning it if it was found. - * - * @return The loaded data if it was found, `null` otherwise. - */ - public abstract suspend fun reload(unit: StorageUnit): R? + /** + * Retrieve the data represented by the given storage unit from persistent storage, storing it in the [dataCache] + * and returning it if it was found. + * + * @return The loaded data if it was found, `null` otherwise. + */ + public abstract suspend fun reload(unit: StorageUnit): R? - /** - * Save the cached data represented by the given storage unit to persistent storage, creating any files and folders - * as needed. - * - * @return The saved data if it was found, `null` otherwise. - */ - public abstract suspend fun save(unit: StorageUnit): R? + /** + * Save the cached data represented by the given storage unit to persistent storage, creating any files and folders + * as needed. + * + * @return The saved data if it was found, `null` otherwise. + */ + public abstract suspend fun save(unit: StorageUnit): R? - /** - * Save the given data represented by the given storage unit to persistent storage, creating any files and folders - * as needed, and storing the given data object in the [dataCache]. - * - * @return The saved data. - */ - public abstract suspend fun save(unit: StorageUnit, data: R): R + /** + * Save the given data represented by the given storage unit to persistent storage, creating any files and folders + * as needed, and storing the given data object in the [dataCache]. + * + * @return The saved data. + */ + public abstract suspend fun save(unit: StorageUnit, data: R): R - /** - * Reload all data objects stored in [dataCache] by calling [reload] against each storage unit. - */ - public open suspend fun reloadAll() { - unitCache.keys.forEach { reload(it) } - } + /** + * Reload all data objects stored in [dataCache] by calling [reload] against each storage unit. + */ + public open suspend fun reloadAll() { + unitCache.keys.forEach { reload(it) } + } - /** - * Save all data objects stored in [dataCache] to persistent storage by calling [save] against each. - */ - public open suspend fun saveAll() { - unitCache.keys.forEach { save(it) } - } + /** + * Save all data objects stored in [dataCache] to persistent storage by calling [save] against each. + */ + public open suspend fun saveAll() { + unitCache.keys.forEach { save(it) } + } - /** - * Convenience function for removing a storage unit from both caches, if required. Will only remove a stored data - * object if all storage units referencing it are removed. - */ - protected open suspend fun removeFromCache(unit: StorageUnit<*>) { - val dataId = unitCache.remove(unit) ?: return - val removeData = unitCache.filterValues { it == dataId }.isEmpty() + /** + * Convenience function for removing a storage unit from both caches, if required. Will only remove a stored data + * object if all storage units referencing it are removed. + */ + protected open suspend fun removeFromCache(unit: StorageUnit<*>) { + val dataId = unitCache.remove(unit) ?: return + val removeData = unitCache.filterValues { it == dataId }.isEmpty() - if (removeData) { - dataCache.remove(dataId) - } - } + if (removeData) { + dataCache.remove(dataId) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/StorageTypeSerializer.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/StorageTypeSerializer.kt index 940bc9b807..150333ed39 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/StorageTypeSerializer.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/StorageTypeSerializer.kt @@ -15,17 +15,17 @@ import kotlinx.serialization.encoding.Encoder /** Simple serializer for the [StorageType] sealed class. **/ public class StorageTypeSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StorageType", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StorageType", PrimitiveKind.STRING) - override fun deserialize(decoder: Decoder): StorageType = - when (val string = decoder.decodeString()) { - StorageType.Config.type -> StorageType.Config - StorageType.Data.type -> StorageType.Data + override fun deserialize(decoder: Decoder): StorageType = + when (val string = decoder.decodeString()) { + StorageType.Config.type -> StorageType.Config + StorageType.Data.type -> StorageType.Data - else -> error("Unknown storage type: $string") - } + else -> error("Unknown storage type: $string") + } - override fun serialize(encoder: Encoder, value: StorageType) { - encoder.encodeString(value.type) - } + override fun serialize(encoder: Encoder, value: StorageType) { + encoder.encodeString(value.type) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/StorageUnit.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/StorageUnit.kt index ee62ef65a0..7c81bf20ed 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/StorageUnit.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/StorageUnit.kt @@ -33,248 +33,248 @@ import kotlin.reflect.KClass */ @Suppress("DataClassContainsFunctions", "DataClassShouldBeImmutable") public open class StorageUnit( - /** The type of data to store. **/ - public open val storageType: StorageType, + /** The type of data to store. **/ + public open val storageType: StorageType, - /** The namespace - usually a plugin or extension ID. Represents a folder for file-backed storage. **/ - public open val namespace: String, + /** The namespace - usually a plugin or extension ID. Represents a folder for file-backed storage. **/ + public open val namespace: String, - /** The identifier - usually a specific category or name. Represents a filename for file-backed storage. **/ - public open val identifier: String, + /** The identifier - usually a specific category or name. Represents a filename for file-backed storage. **/ + public open val identifier: String, - /** The classobj representing your data - usually retrieved via `MyDataClass::class`. **/ - public val dataType: KClass, + /** The classobj representing your data - usually retrieved via `MyDataClass::class`. **/ + public val dataType: KClass, ) : KordExKoinComponent { - /** Storage unit key - used to construct paths, or just as a string reference to this storage unit. **/ - public val unitKey: String = "${storageType.type}/$namespace/$identifier" - - protected val dataAdapter: DataAdapter<*> by inject() - - /** Channel context, supplied via [withChannel] or [withChannelFrom]. **/ - public open var channel: Snowflake? = null - internal set - - /** Guild context, supplied via [withGuild] or [withGuildFrom]. **/ - public open var guild: Snowflake? = null - internal set - - /** Message context, supplied via [withMessage] or [withMessageFrom]. **/ - public open var message: Snowflake? = null - internal set - - /** User context, supplied via [withUser] or [withUserFrom]. **/ - public open var user: Snowflake? = null - internal set - - /** Reference to the serializer for this storage unit's data type. **/ - @OptIn(InternalSerializationApi::class) - public val serializer: KSerializer = dataType.serializer() - - /** - * Convenience function, allowing you to delete the data represented by this storage unit. - * - * @see DataAdapter.delete - */ - public suspend fun delete(): Boolean = - dataAdapter.delete(this) - - /** - * Convenience function, allowing you to retrieve the data represented by this storage unit. - * - * @see DataAdapter.get - */ - public suspend fun get(): T? = - dataAdapter.get(this) - - /** - * Convenience function, allowing you to reload the data represented by this storage unit. - * - * @see DataAdapter.reload - */ - public suspend fun reload(): T? = - dataAdapter.reload(this) - - /** - * Convenience function, allowing you to save the cached data represented by this storage unit. - * - * @see DataAdapter.save - */ - public suspend fun save(): T? = - dataAdapter.save(this) - - /** - * Convenience function, allowing you to save the given data object, as represented by this storage unit. - * - * @see DataAdapter.save - */ - public suspend fun save(data: T): T = - dataAdapter.save(this, data) - - /** - * Copy this [StorageUnit], applying the given channel's ID to its context, but only if it's not a DM channel. - */ - public suspend fun withChannel(channelObj: ChannelBehavior): StorageUnit { - return copy().apply { - if (channelObj.asChannel() !is DmChannel) { - channel = channelObj.id - } - } - } - - /** - * Copy this [StorageUnit], applying the given channel ID to its context, but only if it's not a DM channel. - */ - public fun withChannel(channelId: Snowflake): StorageUnit { - return copy().apply { - channel = channelId - } - } - - /** - * Copy this [StorageUnit], applying the channel ID from the given event to its context, but only if it's present - * and not a DM channel. - */ - public suspend fun withChannelFrom(event: Event): StorageUnit { - return copy().apply { - if (guildFor(event) != null) { - channel = channelFor(event)?.id - } - } - } - - /** - * Copy this [StorageUnit], applying the given guild's ID to its context. - */ - public fun withGuild(guildObj: GuildBehavior): StorageUnit { - return copy().apply { - guild = guildObj.id - } - } - - /** - * Copy this [StorageUnit], applying the given guild ID to its context. - */ - public fun withGuild(guildId: Snowflake): StorageUnit { - return copy().apply { - guild = guildId - } - } - - /** - * Copy this [StorageUnit], applying the guild ID from the given event to its context, if present. - */ - public suspend fun withGuildFrom(event: Event): StorageUnit { - return copy().apply { - guild = guildFor(event)?.id - } - } - - /** - * Copy this [StorageUnit], applying the given message's ID to its context. - */ - public fun withMessage(messageObj: MessageBehavior): StorageUnit { - return copy().apply { - message = messageObj.id - } - } - - /** - * Copy this [StorageUnit], applying the given message ID to its context. - */ - public fun withMessage(messageId: Snowflake): StorageUnit { - return copy().apply { - message = messageId - } - } - - /** - * Copy this [StorageUnit], applying the message ID from the given event to its context, if present. - */ - public suspend fun withMessageFrom(event: Event): StorageUnit { - return copy().apply { - message = messageFor(event)?.id - } - } - - /** - * Copy this [StorageUnit], applying the given user's ID to its context. - */ - public fun withUser(userObj: UserBehavior): StorageUnit { - return copy().apply { - user = userObj.id - } - } - - /** - * Copy this [StorageUnit], applying the given user ID to its context. - */ - public fun withUser(userId: Snowflake): StorageUnit { - return copy().apply { - user = userId - } - } - - /** - * Copy this [StorageUnit], applying the user ID from the given event to its context, if present. - */ - public suspend fun withUserFrom(event: Event): StorageUnit { - return copy().apply { - user = userFor(event)?.id - } - } - - /** Return a new [StorageUnit] object, containing a copy of all the data that's stored in this one. **/ - public open fun copy(): StorageUnit { - val unit = StorageUnit( - storageType = storageType, - namespace = namespace, - identifier = identifier, - dataType = dataType - ) - - unit.channel = channel - unit.guild = guild - unit.message = message - unit.user = user - - return unit - } - - override fun toString(): String = unitKey - - /** Generated function provided here because data classes don't care about non-constructor properties. **/ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as StorageUnit<*> - - if (storageType != other.storageType) return false - if (namespace != other.namespace) return false - if (identifier != other.identifier) return false - if (unitKey != other.unitKey) return false - if (channel != other.channel) return false - if (guild != other.guild) return false - if (message != other.message) return false - if (user != other.user) return false - - return true - } - - /** Generated function provided here because data classes don't care about non-constructor properties. **/ - override fun hashCode(): Int { - var result = storageType.hashCode() - - result = 31 * result + namespace.hashCode() - result = 31 * result + identifier.hashCode() - result = 31 * result + unitKey.hashCode() - result = 31 * result + (channel?.hashCode() ?: 0) - result = 31 * result + (guild?.hashCode() ?: 0) - result = 31 * result + (message?.hashCode() ?: 0) - result = 31 * result + (user?.hashCode() ?: 0) - - return result - } + /** Storage unit key - used to construct paths, or just as a string reference to this storage unit. **/ + public val unitKey: String = "${storageType.type}/$namespace/$identifier" + + protected val dataAdapter: DataAdapter<*> by inject() + + /** Channel context, supplied via [withChannel] or [withChannelFrom]. **/ + public open var channel: Snowflake? = null + internal set + + /** Guild context, supplied via [withGuild] or [withGuildFrom]. **/ + public open var guild: Snowflake? = null + internal set + + /** Message context, supplied via [withMessage] or [withMessageFrom]. **/ + public open var message: Snowflake? = null + internal set + + /** User context, supplied via [withUser] or [withUserFrom]. **/ + public open var user: Snowflake? = null + internal set + + /** Reference to the serializer for this storage unit's data type. **/ + @OptIn(InternalSerializationApi::class) + public val serializer: KSerializer = dataType.serializer() + + /** + * Convenience function, allowing you to delete the data represented by this storage unit. + * + * @see DataAdapter.delete + */ + public suspend fun delete(): Boolean = + dataAdapter.delete(this) + + /** + * Convenience function, allowing you to retrieve the data represented by this storage unit. + * + * @see DataAdapter.get + */ + public suspend fun get(): T? = + dataAdapter.get(this) + + /** + * Convenience function, allowing you to reload the data represented by this storage unit. + * + * @see DataAdapter.reload + */ + public suspend fun reload(): T? = + dataAdapter.reload(this) + + /** + * Convenience function, allowing you to save the cached data represented by this storage unit. + * + * @see DataAdapter.save + */ + public suspend fun save(): T? = + dataAdapter.save(this) + + /** + * Convenience function, allowing you to save the given data object, as represented by this storage unit. + * + * @see DataAdapter.save + */ + public suspend fun save(data: T): T = + dataAdapter.save(this, data) + + /** + * Copy this [StorageUnit], applying the given channel's ID to its context, but only if it's not a DM channel. + */ + public suspend fun withChannel(channelObj: ChannelBehavior): StorageUnit { + return copy().apply { + if (channelObj.asChannel() !is DmChannel) { + channel = channelObj.id + } + } + } + + /** + * Copy this [StorageUnit], applying the given channel ID to its context, but only if it's not a DM channel. + */ + public fun withChannel(channelId: Snowflake): StorageUnit { + return copy().apply { + channel = channelId + } + } + + /** + * Copy this [StorageUnit], applying the channel ID from the given event to its context, but only if it's present + * and not a DM channel. + */ + public suspend fun withChannelFrom(event: Event): StorageUnit { + return copy().apply { + if (guildFor(event) != null) { + channel = channelFor(event)?.id + } + } + } + + /** + * Copy this [StorageUnit], applying the given guild's ID to its context. + */ + public fun withGuild(guildObj: GuildBehavior): StorageUnit { + return copy().apply { + guild = guildObj.id + } + } + + /** + * Copy this [StorageUnit], applying the given guild ID to its context. + */ + public fun withGuild(guildId: Snowflake): StorageUnit { + return copy().apply { + guild = guildId + } + } + + /** + * Copy this [StorageUnit], applying the guild ID from the given event to its context, if present. + */ + public suspend fun withGuildFrom(event: Event): StorageUnit { + return copy().apply { + guild = guildFor(event)?.id + } + } + + /** + * Copy this [StorageUnit], applying the given message's ID to its context. + */ + public fun withMessage(messageObj: MessageBehavior): StorageUnit { + return copy().apply { + message = messageObj.id + } + } + + /** + * Copy this [StorageUnit], applying the given message ID to its context. + */ + public fun withMessage(messageId: Snowflake): StorageUnit { + return copy().apply { + message = messageId + } + } + + /** + * Copy this [StorageUnit], applying the message ID from the given event to its context, if present. + */ + public suspend fun withMessageFrom(event: Event): StorageUnit { + return copy().apply { + message = messageFor(event)?.id + } + } + + /** + * Copy this [StorageUnit], applying the given user's ID to its context. + */ + public fun withUser(userObj: UserBehavior): StorageUnit { + return copy().apply { + user = userObj.id + } + } + + /** + * Copy this [StorageUnit], applying the given user ID to its context. + */ + public fun withUser(userId: Snowflake): StorageUnit { + return copy().apply { + user = userId + } + } + + /** + * Copy this [StorageUnit], applying the user ID from the given event to its context, if present. + */ + public suspend fun withUserFrom(event: Event): StorageUnit { + return copy().apply { + user = userFor(event)?.id + } + } + + /** Return a new [StorageUnit] object, containing a copy of all the data that's stored in this one. **/ + public open fun copy(): StorageUnit { + val unit = StorageUnit( + storageType = storageType, + namespace = namespace, + identifier = identifier, + dataType = dataType + ) + + unit.channel = channel + unit.guild = guild + unit.message = message + unit.user = user + + return unit + } + + override fun toString(): String = unitKey + + /** Generated function provided here because data classes don't care about non-constructor properties. **/ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StorageUnit<*> + + if (storageType != other.storageType) return false + if (namespace != other.namespace) return false + if (identifier != other.identifier) return false + if (unitKey != other.unitKey) return false + if (channel != other.channel) return false + if (guild != other.guild) return false + if (message != other.message) return false + if (user != other.user) return false + + return true + } + + /** Generated function provided here because data classes don't care about non-constructor properties. **/ + override fun hashCode(): Int { + var result = storageType.hashCode() + + result = 31 * result + namespace.hashCode() + result = 31 * result + identifier.hashCode() + result = 31 * result + unitKey.hashCode() + result = 31 * result + (channel?.hashCode() ?: 0) + result = 31 * result + (guild?.hashCode() ?: 0) + result = 31 * result + (message?.hashCode() ?: 0) + result = 31 * result + (user?.hashCode() ?: 0) + + return result + } } /** @@ -287,13 +287,13 @@ public open class StorageUnit( */ @Suppress("FunctionName") public inline fun StorageUnit( - storageType: StorageType, - namespace: String, - identifier: String + storageType: StorageType, + namespace: String, + identifier: String, ): StorageUnit = - StorageUnit( - storageType, - namespace, - identifier, - T::class - ) + StorageUnit( + storageType, + namespace, + identifier, + T::class + ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/_Utils.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/_Utils.kt index f1abf7fc52..05b20ba7a2 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/_Utils.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/_Utils.kt @@ -15,5 +15,5 @@ import kotlin.io.path.Path * directory, but you can change this by providing the `STORAGE_FILE_ROOT` environment variable. */ public val storageFileRoot: Path = Path( - envOrNull("STORAGE_FILE_ROOT") ?: "." + envOrNull("STORAGE_FILE_ROOT") ?: "." ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/toml/TomlDataAdapter.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/toml/TomlDataAdapter.kt index 0476c73eb9..0eaa4a36be 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/toml/TomlDataAdapter.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/storage/toml/TomlDataAdapter.kt @@ -15,7 +15,6 @@ import com.kotlindiscord.kord.extensions.storage.storageFileRoot import net.peanuuutz.tomlkt.Toml import java.io.File import java.nio.file.Path -import kotlin.io.path.Path import kotlin.io.path.div import kotlin.io.path.pathString @@ -25,92 +24,92 @@ import kotlin.io.path.pathString * This is a pretty simple implementation, so it's a good example to use when writing your own data adapters. */ public open class TomlDataAdapter : DataAdapter() { - private val StorageUnit<*>.file: File - get() = getPath().toFile() + private val StorageUnit<*>.file: File + get() = getPath().toFile() - private val StorageUnit<*>.pathString: String - get() = getPath().pathString + private val StorageUnit<*>.pathString: String + get() = getPath().pathString - private fun StorageUnit<*>.getPath(): Path { - var path = storageFileRoot / storageType.type / namespace + private fun StorageUnit<*>.getPath(): Path { + var path = storageFileRoot / storageType.type / namespace - if (guild != null) path /= "guild-$guild" - if (channel != null) path /= "channel-$channel" - if (user != null) path /= "user-$user" - if (message != null) path /= "message-$message" + if (guild != null) path /= "guild-$guild" + if (channel != null) path /= "channel-$channel" + if (user != null) path /= "user-$user" + if (message != null) path /= "message-$message" - return path / "$identifier.toml" - } + return path / "$identifier.toml" + } - override suspend fun delete(unit: StorageUnit): Boolean { - removeFromCache(unit) + override suspend fun delete(unit: StorageUnit): Boolean { + removeFromCache(unit) - val file = unit.file + val file = unit.file - if (file.exists()) { - return file.delete() - } + if (file.exists()) { + return file.delete() + } - return false - } + return false + } - override suspend fun get(unit: StorageUnit): R? { - val dataId = unitCache[unit] + override suspend fun get(unit: StorageUnit): R? { + val dataId = unitCache[unit] - if (dataId != null) { - val data = dataCache[dataId] + if (dataId != null) { + val data = dataCache[dataId] - if (data != null) { - return data as R - } - } + if (data != null) { + return data as R + } + } - return reload(unit) - } + return reload(unit) + } - override suspend fun reload(unit: StorageUnit): R? { - val dataId = unit.pathString - val file = unit.file + override suspend fun reload(unit: StorageUnit): R? { + val dataId = unit.pathString + val file = unit.file - if (file.exists()) { - val result: R = Toml.decodeFromString(unit.serializer, file.readText()) + if (file.exists()) { + val result: R = Toml.decodeFromString(unit.serializer, file.readText()) - dataCache[dataId] = result - unitCache[unit] = dataId - } + dataCache[dataId] = result + unitCache[unit] = dataId + } - return dataCache[dataId] as R? - } + return dataCache[dataId] as R? + } - override suspend fun save(unit: StorageUnit, data: R): R { - val dataId = unit.pathString + override suspend fun save(unit: StorageUnit, data: R): R { + val dataId = unit.pathString - dataCache[dataId] = data - unitCache[unit] = dataId + dataCache[dataId] = data + unitCache[unit] = dataId - val file = unit.file + val file = unit.file - if (!file.exists()) { - file.parentFile?.mkdirs() - file.createNewFile() - } + if (!file.exists()) { + file.parentFile?.mkdirs() + file.createNewFile() + } - file.writeText(Toml.encodeToString(unit.serializer, data)) + file.writeText(Toml.encodeToString(unit.serializer, data)) - return data - } + return data + } - override suspend fun save(unit: StorageUnit): R? { - val data = get(unit) ?: return null - val file = unit.file + override suspend fun save(unit: StorageUnit): R? { + val data = get(unit) ?: return null + val file = unit.file - if (!file.exists()) { - file.parentFile?.mkdirs() - file.createNewFile() - } + if (!file.exists()) { + file.parentFile?.mkdirs() + file.createNewFile() + } - file.writeText(Toml.encodeToString(unit.serializer, data)) + file.writeText(Toml.encodeToString(unit.serializer, data)) - return data - } + return data + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/time/TimestampType.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/time/TimestampType.kt index e000f01c57..d7894abcd0 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/time/TimestampType.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/time/TimestampType.kt @@ -12,49 +12,49 @@ package com.kotlindiscord.kord.extensions.time * @property string Suffix to add to the string to get Discord's respective format, with colon prefix */ public sealed class TimestampType(public val string: String?) { - /** Let Discord figure out what format to use. **/ - public object Default : TimestampType(null) - - /** A short date and time. **/ - public object ShortDateTime : TimestampType(":f") - - /** A long date and time. **/ - public object LongDateTime : TimestampType(":F") - - /** A short date. **/ - public object ShortDate : TimestampType(":d") - - /** A long date. **/ - public object LongDate : TimestampType(":D") - - /** A short time. **/ - public object ShortTime : TimestampType(":t") - - /** A long time. **/ - public object LongTime : TimestampType(":T") - - /** A time, displayed relative to the current time. **/ - public object RelativeTime : TimestampType(":R") - - /** Format the given [Long] value according to the current timestamp type. **/ - public fun format(value: Long): String = "" - - public companion object { - /** - * Parse Discord's format specifiers to a specific format. - */ - public fun fromFormatSpecifier(string: String?): TimestampType? { - return when (string) { - "f" -> ShortDateTime - "F" -> LongDateTime - "d" -> ShortDate - "D" -> LongDate - "t" -> ShortTime - "T" -> LongTime - "R" -> RelativeTime - null -> Default - else -> null - } - } - } + /** Let Discord figure out what format to use. **/ + public object Default : TimestampType(null) + + /** A short date and time. **/ + public object ShortDateTime : TimestampType(":f") + + /** A long date and time. **/ + public object LongDateTime : TimestampType(":F") + + /** A short date. **/ + public object ShortDate : TimestampType(":d") + + /** A long date. **/ + public object LongDate : TimestampType(":D") + + /** A short time. **/ + public object ShortTime : TimestampType(":t") + + /** A long time. **/ + public object LongTime : TimestampType(":T") + + /** A time, displayed relative to the current time. **/ + public object RelativeTime : TimestampType(":R") + + /** Format the given [Long] value according to the current timestamp type. **/ + public fun format(value: Long): String = "" + + public companion object { + /** + * Parse Discord's format specifiers to a specific format. + */ + public fun fromFormatSpecifier(string: String?): TimestampType? { + return when (string) { + "f" -> ShortDateTime + "F" -> LongDateTime + "d" -> ShortDate + "D" -> LongDate + "t" -> ShortTime + "T" -> LongTime + "R" -> RelativeTime + null -> Default + else -> null + } + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/time/Utils.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/time/Utils.kt index 7ff8dc4881..0612142cad 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/time/Utils.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/time/Utils.kt @@ -17,36 +17,37 @@ public fun Instant.toDiscord(format: TimestampType): String = format.format(epoc /** Retrieve the [DateTimeUnit] for the given pluralized English name. **/ public fun namedDateTimeUnit(name: String): DateTimeUnit = when (name) { - "nanoseconds" -> DateTimeUnit.NANOSECOND - "microseconds" -> DateTimeUnit.MICROSECOND - "milliseconds" -> DateTimeUnit.MILLISECOND - "seconds" -> DateTimeUnit.SECOND - "minutes" -> DateTimeUnit.MINUTE - "hours" -> DateTimeUnit.HOUR - "days" -> DateTimeUnit.DAY - "weeks" -> DateTimeUnit.WEEK - "months" -> DateTimeUnit.MONTH - "quarters" -> DateTimeUnit.QUARTER - "years" -> DateTimeUnit.YEAR - "centuries" -> DateTimeUnit.CENTURY + "nanoseconds" -> DateTimeUnit.NANOSECOND + "microseconds" -> DateTimeUnit.MICROSECOND + "milliseconds" -> DateTimeUnit.MILLISECOND + "seconds" -> DateTimeUnit.SECOND + "minutes" -> DateTimeUnit.MINUTE + "hours" -> DateTimeUnit.HOUR + "days" -> DateTimeUnit.DAY + "weeks" -> DateTimeUnit.WEEK + "months" -> DateTimeUnit.MONTH + "quarters" -> DateTimeUnit.QUARTER + "years" -> DateTimeUnit.YEAR + "centuries" -> DateTimeUnit.CENTURY - else -> error("Unsupported unit name: $name") + else -> error("Unsupported unit name: $name") } /** Retrieve the pluralized English name for a given [DateTimeUnit]. **/ -public val DateTimeUnit.name: String get() = when (this) { - DateTimeUnit.NANOSECOND -> "nanoseconds" - DateTimeUnit.MICROSECOND -> "microseconds" - DateTimeUnit.MILLISECOND -> "milliseconds" - DateTimeUnit.SECOND -> "seconds" - DateTimeUnit.MINUTE -> "minutes" - DateTimeUnit.HOUR -> "hours" - DateTimeUnit.DAY -> "days" - DateTimeUnit.WEEK -> "weeks" - DateTimeUnit.MONTH -> "months" - DateTimeUnit.QUARTER -> "quarters" - DateTimeUnit.YEAR -> "years" - DateTimeUnit.CENTURY -> "centuries" +public val DateTimeUnit.name: String + get() = when (this) { + DateTimeUnit.NANOSECOND -> "nanoseconds" + DateTimeUnit.MICROSECOND -> "microseconds" + DateTimeUnit.MILLISECOND -> "milliseconds" + DateTimeUnit.SECOND -> "seconds" + DateTimeUnit.MINUTE -> "minutes" + DateTimeUnit.HOUR -> "hours" + DateTimeUnit.DAY -> "days" + DateTimeUnit.WEEK -> "weeks" + DateTimeUnit.MONTH -> "months" + DateTimeUnit.QUARTER -> "quarters" + DateTimeUnit.YEAR -> "years" + DateTimeUnit.CENTURY -> "centuries" - else -> error("Unsupported unit: $this") -} + else -> error("Unsupported unit: $this") + } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/FailureReason.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/FailureReason.kt index 632b188f90..c0051109b1 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/FailureReason.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/FailureReason.kt @@ -18,27 +18,27 @@ import com.kotlindiscord.kord.extensions.DiscordRelayedException * @param error Throwable that triggered this failure, if any. */ public sealed class FailureReason(public val error: E) { - /** Sealed class representing a basic check failure. **/ - public sealed class BaseCheckFailure(error: E) : - FailureReason(error) + /** Sealed class representing a basic check failure. **/ + public sealed class BaseCheckFailure(error: E) : + FailureReason(error) - /** Type representing an error thrown during command/component execution. **/ - public class ExecutionError(error: Throwable) : - FailureReason(error) + /** Type representing an error thrown during command/component execution. **/ + public class ExecutionError(error: Throwable) : + FailureReason(error) - /** Type representing a relayed exception that was thrown during command execution. **/ - public class RelayedFailure(error: DiscordRelayedException) : - FailureReason(error) + /** Type representing a relayed exception that was thrown during command execution. **/ + public class RelayedFailure(error: DiscordRelayedException) : + FailureReason(error) - /** Type representing an argument parsing failure, for command types with arguments. **/ - public class ArgumentParsingFailure(error: ArgumentParsingException) : - FailureReason(error) + /** Type representing an argument parsing failure, for command types with arguments. **/ + public class ArgumentParsingFailure(error: ArgumentParsingException) : + FailureReason(error) - /** Type representing a standard "provided" check failure (provided via `check {}`). **/ - public class ProvidedCheckFailure(error: DiscordRelayedException) : - BaseCheckFailure(error) + /** Type representing a standard "provided" check failure (provided via `check {}`). **/ + public class ProvidedCheckFailure(error: DiscordRelayedException) : + BaseCheckFailure(error) - /** Type representing a failure caused by the bot having insufficient permissions. **/ - public class OwnPermissionsCheckFailure(error: DiscordRelayedException) : - BaseCheckFailure(error) + /** Type representing a failure caused by the bot having insufficient permissions. **/ + public class OwnPermissionsCheckFailure(error: DiscordRelayedException) : + BaseCheckFailure(error) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/Lockable.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/Lockable.kt index 3dd2f6cd5b..53cffabe3c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/Lockable.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/Lockable.kt @@ -10,34 +10,34 @@ import kotlinx.coroutines.sync.Mutex /** Interface representing something with a [Mutex] that can be locked. **/ public interface Lockable { - /** Mutex object to use for locking. **/ - public var mutex: Mutex? - - /** Set this to `true` to lock execution with a [Mutex]. **/ - public var locking: Boolean - - /** Lock the mutex (if locking is enabled), call the supplied callable, and unlock. **/ - public suspend fun withLock(body: suspend () -> T) { - try { - lock() - - body() - } finally { - unlock() - } - } - - /** Lock the mutex, if locking is enabled - suspending until it's unlocked. **/ - public suspend fun lock() { - if (locking) { - mutex?.lock() - } - } - - /** Unlock the mutex, if it's locked. **/ - public fun unlock() { - if (locking) { - mutex?.unlock() - } - } + /** Mutex object to use for locking. **/ + public var mutex: Mutex? + + /** Set this to `true` to lock execution with a [Mutex]. **/ + public var locking: Boolean + + /** Lock the mutex (if locking is enabled), call the supplied callable, and unlock. **/ + public suspend fun withLock(body: suspend () -> T) { + try { + lock() + + body() + } finally { + unlock() + } + } + + /** Lock the mutex, if locking is enabled - suspending until it's unlocked. **/ + public suspend fun lock() { + if (locking) { + mutex?.lock() + } + } + + /** Unlock the mutex, if it's locked. **/ + public fun unlock() { + if (locking) { + mutex?.unlock() + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/TranslatableContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/TranslatableContext.kt index 4062bd3eb0..6b20755a6b 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/TranslatableContext.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/types/TranslatableContext.kt @@ -17,57 +17,57 @@ import java.util.* * implemented by the extending type. */ public interface TranslatableContext { - /** Cached locale variable, stored and retrieved by [getLocale]. **/ - public var resolvedLocale: Locale? + /** Cached locale variable, stored and retrieved by [getLocale]. **/ + public var resolvedLocale: Locale? - /** Default bundle to use for the [translate] functions. **/ - public val bundle: String? + /** Default bundle to use for the [translate] functions. **/ + public val bundle: String? - /** Retrieve the bot's translation provider from Koin. **/ - public fun getTranslationProvider(): TranslationsProvider = KordExContext.get().get() + /** Retrieve the bot's translation provider from Koin. **/ + public fun getTranslationProvider(): TranslationsProvider = KordExContext.get().get() - /** Resolve the locale for this context, storing it in [resolvedLocale]. **/ - public suspend fun getLocale(): Locale + /** Resolve the locale for this context, storing it in [resolvedLocale]. **/ + public suspend fun getLocale(): Locale - /** - * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured - * locale resolvers. - */ - public suspend fun translate( - key: String, - bundleName: String?, - replacements: Array = arrayOf(), - ): String + /** + * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured + * locale resolvers. + */ + public suspend fun translate( + key: String, + bundleName: String?, + replacements: Array = arrayOf(), + ): String - /** - * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured - * locale resolvers. - */ - public suspend fun translate( - key: String, - bundleName: String?, - replacements: Map, - ): String + /** + * Given a translation key and bundle name, return the translation for the locale provided by the bot's configured + * locale resolvers. + */ + public suspend fun translate( + key: String, + bundleName: String?, + replacements: Map, + ): String - /** - * Given a translation key, return the translation for the locale provided by the bot's configured locale - * resolvers, using the bundle provided for this context. - */ - public suspend fun translate( - key: String, - replacements: Array = arrayOf(), - ): String = translate( - key, bundle, replacements - ) + /** + * Given a translation key, return the translation for the locale provided by the bot's configured locale + * resolvers, using the bundle provided for this context. + */ + public suspend fun translate( + key: String, + replacements: Array = arrayOf(), + ): String = translate( + key, bundle, replacements + ) - /** - * Given a translation key, return the translation for the locale provided by the bot's configured locale - * resolvers, using the bundle provided for this context. - */ - public suspend fun translate( - key: String, - replacements: Map, - ): String = translate( - key, bundle, replacements - ) + /** + * Given a translation key, return the translation for the locale provided by the bot's configured locale + * resolvers, using the bundle provided for this context. + */ + public suspend fun translate( + key: String, + replacements: Map, + ): String = translate( + key, bundle, replacements + ) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/Mentions.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/Mentions.kt index 15ed732321..2a296366e9 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/Mentions.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/Mentions.kt @@ -8,12 +8,12 @@ package com.kotlindiscord.kord.extensions.utils /** Object with easy access to unusual mentionable sections of Discord. **/ public object Mentions { - /** A clickable mention that takes you to this server's home tab. **/ - public const val GuildHome: String = "" + /** A clickable mention that takes you to this server's home tab. **/ + public const val GuildHome: String = "" - /** A clickable mention that takes you to this server's "browse channels" tab. **/ - public const val GuildBrowseChannels: String = "" + /** A clickable mention that takes you to this server's "browse channels" tab. **/ + public const val GuildBrowseChannels: String = "" - /** A clickable mention that takes you to this server's "customize community"/onboarding tab. **/ - public const val GuildCustomizeCommunity: String = "" + /** A clickable mention that takes you to this server's "customize community"/onboarding tab. **/ + public const val GuildCustomizeCommunity: String = "" } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Attachments.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Attachments.kt index bf475236b4..09157a1046 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Attachments.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Attachments.kt @@ -17,7 +17,7 @@ import java.nio.file.Path import kotlin.io.path.* private val client = HttpClient { - expectSuccess = true + expectSuccess = true } /** @@ -27,10 +27,10 @@ private val client = HttpClient { * of the other functions. */ public suspend fun Attachment.download(): ByteArray { - val channel = client.get(this.url) - val packet = channel.bodyAsChannel() + val channel = client.get(this.url) + val packet = channel.bodyAsChannel() - return packet.readRemaining().readBytes() + return packet.readRemaining().readBytes() } /** Given a [String] representing a file path, download the attachment to the file it points to. **/ @@ -38,50 +38,50 @@ public suspend fun Attachment.downloadToFile(path: String): Path = downloadToFil /** Given a [Path] object, download the attachment to the file it points to. **/ public suspend fun Attachment.downloadToFile(path: Path): Path { - if (!path.exists()) { - path.createDirectories() - path.deleteExisting() - path.createFile() - } + if (!path.exists()) { + path.createDirectories() + path.deleteExisting() + path.createFile() + } - return downloadToFile(path.toFile()) + return downloadToFile(path.toFile()) } /** Given a [File] object, download the attachment and write it to the given file. **/ public suspend fun Attachment.downloadToFile(file: File): Path { - if (!file.exists()) { - file.toPath().createFile() - } + if (!file.exists()) { + file.toPath().createFile() + } - val channel = client.get(this.url).bodyAsChannel() + val channel = client.get(this.url).bodyAsChannel() - file.outputStream().use { fileStream -> - channel.copyTo(fileStream) - } + file.outputStream().use { fileStream -> + channel.copyTo(fileStream) + } - return file.toPath() + return file.toPath() } /** Given a [String] representing a folder path, download the attachment to a file within it. **/ public suspend fun Attachment.downloadToFolder(path: String): Path = - downloadToFolder(Path.of(path)) + downloadToFolder(Path.of(path)) /** Given a [Path] representing a folder, download the attachment to a file within it. **/ public suspend fun Attachment.downloadToFolder(path: Path): Path = - downloadToFolder(path.toFile()) + downloadToFolder(path.toFile()) /** Given a [File] representing a folder, download the attachment to a file within it. **/ public suspend fun Attachment.downloadToFolder(file: File): Path { - if (!file.exists()) { - file.toPath().createDirectories() - } + if (!file.exists()) { + file.toPath().createDirectories() + } - val targetFile = File(file, "${this.id.value} - ${this.filename}") - val channel = client.get(this.url).bodyAsChannel() + val targetFile = File(file, "${this.id.value} - ${this.filename}") + val channel = client.get(this.url).bodyAsChannel() - targetFile.outputStream().use { fileStream -> - channel.copyTo(fileStream) - } + targetFile.outputStream().use { fileStream -> + channel.copyTo(fileStream) + } - return targetFile.toPath() + return targetFile.toPath() } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Channel.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Channel.kt index 85e29867fa..c5ef931cc3 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Channel.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Channel.kt @@ -39,26 +39,26 @@ private val logger = KotlinLogging.logger {} * @return Webhook object for the newly created webhook, or the existing one if it's already there. */ public suspend fun ensureWebhook( - channelObj: TopGuildMessageChannel, - name: String, - logoFormat: Image.Format = Image.Format.PNG, - logo: (suspend () -> ByteArray)? = null + channelObj: TopGuildMessageChannel, + name: String, + logoFormat: Image.Format = Image.Format.PNG, + logo: (suspend () -> ByteArray)? = null, ): Webhook { - val webhook = channelObj.webhooks.firstOrNull { it.name == name } + val webhook = channelObj.webhooks.firstOrNull { it.name == name } - if (webhook != null) { - return webhook - } + if (webhook != null) { + return webhook + } - val guild = channelObj.guild.asGuild() + val guild = channelObj.guild.asGuild() - logger.info { "Creating webhook for channel: #${channelObj.name} (Guild: ${guild.name}" } + logger.info { "Creating webhook for channel: #${channelObj.name} (Guild: ${guild.name}" } - return channelObj.createWebhook(name) { - if (logo != null) { - this.avatar = Image.raw(logo.invoke(), logoFormat) - } - } + return channelObj.createWebhook(name) { + if (logo != null) { + this.avatar = Image.raw(logo.invoke(), logoFormat) + } + } } /** @@ -68,10 +68,10 @@ public suspend fun ensureWebhook( * @param memberId Member ID to calculate for */ public suspend fun GuildChannel.permissionsForMember(memberId: Snowflake): Permissions = when (this) { - is TopGuildChannel -> getEffectivePermissions(memberId) - is ThreadChannel -> getParent().getEffectivePermissions(memberId) + is TopGuildChannel -> getEffectivePermissions(memberId) + is ThreadChannel -> getParent().getEffectivePermissions(memberId) - else -> error("Unsupported channel type for channel: $this") + else -> error("Unsupported channel type for channel: $this") } /** @@ -81,7 +81,7 @@ public suspend fun GuildChannel.permissionsForMember(memberId: Snowflake): Permi * @param user User to calculate for */ public suspend fun GuildChannel.permissionsForMember(user: UserBehavior): Permissions = - permissionsForMember(user.id) + permissionsForMember(user.id) /** * Convenience function that returns the thread's parent message, if it was created from one. @@ -89,9 +89,9 @@ public suspend fun GuildChannel.permissionsForMember(user: UserBehavior): Permis * If it wasn't, the parent channel is a forum, or the parent channel can't be found, this function returns `null`. */ public suspend fun ThreadChannel.getParentMessage(): Message? { - val parentChannel = getParentOrNull() as? MessageChannelBehavior ?: return null + val parentChannel = getParentOrNull() as? MessageChannelBehavior ?: return null - return parentChannel.getMessageOrNull(this.id) + return parentChannel.getMessageOrNull(this.id) } // region: Channel position utils diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_ChannelType.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_ChannelType.kt index 3b1e51bc5e..d1dcb4d882 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_ChannelType.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_ChannelType.kt @@ -13,31 +13,31 @@ import java.util.* /** Given a [ChannelType], return a string representing its translation key. **/ public fun ChannelType.toTranslationKey(): String = when (this) { - ChannelType.DM -> "channelType.dm" - ChannelType.GroupDM -> "channelType.groupDm" - ChannelType.GuildCategory -> "channelType.guildCategory" - ChannelType.GuildNews -> "channelType.guildNews" - ChannelType.GuildStageVoice -> "channelType.guildStageVoice" - ChannelType.GuildText -> "channelType.guildText" - ChannelType.GuildVoice -> "channelType.guildVoice" - ChannelType.PublicNewsThread -> "channelType.publicNewsThread" - ChannelType.PublicGuildThread -> "channelType.publicGuildThread" - ChannelType.PrivateThread -> "channelType.privateThread" - ChannelType.GuildDirectory -> "channelType.guildDirectory" - ChannelType.GuildForum -> "channelType.guildForum" + ChannelType.DM -> "channelType.dm" + ChannelType.GroupDM -> "channelType.groupDm" + ChannelType.GuildCategory -> "channelType.guildCategory" + ChannelType.GuildNews -> "channelType.guildNews" + ChannelType.GuildStageVoice -> "channelType.guildStageVoice" + ChannelType.GuildText -> "channelType.guildText" + ChannelType.GuildVoice -> "channelType.guildVoice" + ChannelType.PublicNewsThread -> "channelType.publicNewsThread" + ChannelType.PublicGuildThread -> "channelType.publicGuildThread" + ChannelType.PrivateThread -> "channelType.privateThread" + ChannelType.GuildDirectory -> "channelType.guildDirectory" + ChannelType.GuildForum -> "channelType.guildForum" ChannelType.GuildMedia -> "channelType.guildMedia" - is ChannelType.Unknown -> "channelType.unknown" + is ChannelType.Unknown -> "channelType.unknown" } /** * Given a [CommandContext], translate the [ChannelType] to a human-readable string based on the context's locale. */ public suspend fun ChannelType.translate(context: CommandContext): String = - context.translate(toTranslationKey()) + context.translate(toTranslationKey()) /** * Given a locale, translate the [ChannelType] to a human-readable string. */ public fun ChannelType.translate(locale: Locale): String = - getKoin().get().translate(toTranslationKey(), locale) + getKoin().get().translate(toTranslationKey(), locale) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Components.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Components.kt index 07a2919d67..e85ae03f67 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Components.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Components.kt @@ -20,56 +20,56 @@ import dev.kord.rest.builder.component.SelectOptionBuilder /** Convenience wrapper for sending an ephemeral ack, optionally deferred, with less characters. **/ public suspend fun ComponentInteractionBehavior.ackEphemeral( - deferred: Boolean = false + deferred: Boolean = false, ): EphemeralMessageInteractionResponseBehavior = if (deferred) { - deferEphemeralMessageUpdate() + deferEphemeralMessageUpdate() } else { - deferEphemeralResponseUnsafe() + deferEphemeralResponseUnsafe() } /** Convenience wrapper for sending a public ack, optionally deferred, with less characters. **/ public suspend fun ComponentInteractionBehavior.ackPublic( - deferred: Boolean = false + deferred: Boolean = false, ): PublicMessageInteractionResponseBehavior = if (deferred) { - deferPublicMessageUpdate() + deferPublicMessageUpdate() } else { - deferPublicResponseUnsafe() + deferPublicResponseUnsafe() } /** Convenience function for setting [this.emoji] based on a given Unicode emoji. **/ public fun SelectOptionBuilder.emoji(unicodeEmoji: String) { - this.emoji = DiscordPartialEmoji( - name = unicodeEmoji - ) + this.emoji = DiscordPartialEmoji( + name = unicodeEmoji + ) } /** Convenience function for setting [this.emoji] based on a given guild custom emoji. **/ public fun SelectOptionBuilder.emoji(guildEmoji: GuildEmoji) { - this.emoji = DiscordPartialEmoji( - id = guildEmoji.id, - name = guildEmoji.name, - animated = guildEmoji.isAnimated.optional() - ) + this.emoji = DiscordPartialEmoji( + id = guildEmoji.id, + name = guildEmoji.name, + animated = guildEmoji.isAnimated.optional() + ) } /** Convenience function for setting [this.emoji] based on a given reaction emoji. **/ public fun SelectOptionBuilder.emoji(unicodeEmoji: ReactionEmoji.Unicode) { - this.emoji = DiscordPartialEmoji( - name = unicodeEmoji.name - ) + this.emoji = DiscordPartialEmoji( + name = unicodeEmoji.name + ) } /** Convenience function for setting [this.emoji] based on a given reaction emoji. **/ public fun SelectOptionBuilder.emoji(guildEmoji: ReactionEmoji.Custom) { - this.emoji = DiscordPartialEmoji( - id = guildEmoji.id, - name = guildEmoji.name, - animated = guildEmoji.isAnimated.optional() - ) + this.emoji = DiscordPartialEmoji( + id = guildEmoji.id, + name = guildEmoji.name, + animated = guildEmoji.isAnimated.optional() + ) } /** Convenience function for setting [this.emoji] based on a given reaction emoji. **/ public fun SelectOptionBuilder.emoji(emoji: ReactionEmoji): Unit = when (emoji) { - is ReactionEmoji.Unicode -> emoji(emoji) - is ReactionEmoji.Custom -> emoji(emoji) + is ReactionEmoji.Unicode -> emoji(emoji) + is ReactionEmoji.Custom -> emoji(emoji) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Durations.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Durations.kt index 1767c310fa..fc0a11c548 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Durations.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Durations.kt @@ -17,20 +17,20 @@ import java.util.* */ @Suppress("MagicNumber") public fun DateTimePeriod.format(locale: Locale): String { - val fmt = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE) - val measures: MutableList = mutableListOf() + val fmt = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE) + val measures: MutableList = mutableListOf() - val weeks = days.floorDiv(7) - val remainingDays = days % 7 + val weeks = days.floorDiv(7) + val remainingDays = days % 7 - if (years != 0) measures.add(Measure(years, MeasureUnit.YEAR)) - if (months != 0) measures.add(Measure(months, MeasureUnit.MONTH)) - if (weeks != 0) measures.add(Measure(weeks, MeasureUnit.WEEK)) - if (remainingDays != 0) measures.add(Measure(remainingDays, MeasureUnit.DAY)) - if (hours != 0) measures.add(Measure(hours, MeasureUnit.HOUR)) - if (minutes != 0) measures.add(Measure(minutes, MeasureUnit.MINUTE)) - if (seconds != 0) measures.add(Measure(seconds, MeasureUnit.SECOND)) + if (years != 0) measures.add(Measure(years, MeasureUnit.YEAR)) + if (months != 0) measures.add(Measure(months, MeasureUnit.MONTH)) + if (weeks != 0) measures.add(Measure(weeks, MeasureUnit.WEEK)) + if (remainingDays != 0) measures.add(Measure(remainingDays, MeasureUnit.DAY)) + if (hours != 0) measures.add(Measure(hours, MeasureUnit.HOUR)) + if (minutes != 0) measures.add(Measure(minutes, MeasureUnit.MINUTE)) + if (seconds != 0) measures.add(Measure(seconds, MeasureUnit.SECOND)) - @Suppress("SpreadOperator") // There's no other way, really - return fmt.formatMeasures(*measures.toTypedArray()) + @Suppress("SpreadOperator") // There's no other way, really + return fmt.formatMeasures(*measures.toTypedArray()) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Environment.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Environment.kt index 7acb869ad9..31ffcd822c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Environment.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Environment.kt @@ -26,57 +26,57 @@ private val envMap: MutableStringMap = mutableMapOf() * @return The value of the environmental variable, or `null` if it doesn't exist. */ public fun envOrNull(name: String): String? { - if (firstLoad) { - firstLoad = false + if (firstLoad) { + firstLoad = false - val dotenvFile = Path(".env") + val dotenvFile = Path(".env") - if (dotenvFile.isRegularFile()) { - logger.info { "Loading environment variables from .env file" } + if (dotenvFile.isRegularFile()) { + logger.info { "Loading environment variables from .env file" } - val lines = dotenvFile.readLines() + val lines = dotenvFile.readLines() - for (line in lines) { - var effectiveLine = line.trimStart() + for (line in lines) { + var effectiveLine = line.trimStart() - if (effectiveLine.isBlank() || effectiveLine.startsWith("#")) { - continue - } + if (effectiveLine.isBlank() || effectiveLine.startsWith("#")) { + continue + } - if (effectiveLine.contains("#")) { - effectiveLine = effectiveLine.substring(0, effectiveLine.indexOf("#")) - } + if (effectiveLine.contains("#")) { + effectiveLine = effectiveLine.substring(0, effectiveLine.indexOf("#")) + } - if (!effectiveLine.contains('=')) { - logger.warn { - "Invalid line in dotenv file: \"=\" not found\n" + - " $effectiveLine" - } + if (!effectiveLine.contains('=')) { + logger.warn { + "Invalid line in dotenv file: \"=\" not found\n" + + " $effectiveLine" + } - continue - } + continue + } - val split = effectiveLine - .split("=", limit = 2) - .map { it.trim() } + val split = effectiveLine + .split("=", limit = 2) + .map { it.trim() } - if (split.size != 2) { - logger.warn { - "Invalid line in dotenv file: variables must be of the form \"name=value\"\n" + - " -> $effectiveLine" - } + if (split.size != 2) { + logger.warn { + "Invalid line in dotenv file: variables must be of the form \"name=value\"\n" + + " -> $effectiveLine" + } - continue - } + continue + } - logger.trace { "${split[0]} -> ${split[1]}" } + logger.trace { "${split[0]} -> ${split[1]}" } - envMap[split[0]] = split[1] - } - } - } + envMap[split[0]] = split[1] + } + } + } - return envMap[name] ?: System.getenv()[name] + return envMap[name] ?: System.getenv()[name] } /** @@ -94,7 +94,7 @@ public fun envOrNull(name: String): String? { * @return The value of the environmental variable. */ public fun env(name: String): String = - envOrNull(name) ?: error( - "Missing environmental variable '$name' - please set this by adding it to a `.env` file, or using your " + - "system or process manager's environment management commands and tools." - ) + envOrNull(name) ?: error( + "Missing environmental variable '$name' - please set this by adding it to a `.env` file, or using your " + + "system or process manager's environment management commands and tools." + ) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Guilds.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Guilds.kt index 7bf94653d5..b2f52f2ceb 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Guilds.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Guilds.kt @@ -26,7 +26,7 @@ public suspend fun GuildBehavior.selfMember(): Member = getMember(kord.selfId) * @see GuildBehavior.botHasPermissions */ public suspend fun GuildChannel.botHasPermissions(vararg requiredPermissions: Permission): Boolean = - guild.botHasPermissions(this, Permissions(requiredPermissions.asIterable())) + guild.botHasPermissions(this, Permissions(requiredPermissions.asIterable())) /** * Checks whether the bot globally has at least [requiredPermissions] on this guild. @@ -34,13 +34,13 @@ public suspend fun GuildChannel.botHasPermissions(vararg requiredPermissions: Pe * @see GuildChannel.botHasPermissions */ public suspend fun GuildBehavior.botHasPermissions(vararg requiredPermissions: Permission): Boolean = - botHasPermissions(null, Permissions(requiredPermissions.asIterable())) + botHasPermissions(null, Permissions(requiredPermissions.asIterable())) private suspend fun GuildBehavior.botHasPermissions(channel: GuildChannel?, requiredPermissions: Permissions): Boolean { - val selfMember = selfMember() - val effectivePermissions = - channel?.run { permissionsForMember(selfMember) } - ?: selfMember.getPermissions() + val selfMember = selfMember() + val effectivePermissions = + channel?.run { permissionsForMember(selfMember) } + ?: selfMember.getPermissions() - return requiredPermissions in effectivePermissions + return requiredPermissions in effectivePermissions } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Interactions.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Interactions.kt index 8611fde31b..f8fc4c2516 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Interactions.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Interactions.kt @@ -22,50 +22,50 @@ public const val MAX_SUGGESTIONS: Int = 25 * @property test Lambda that should return `true` for acceptable values. */ public open class FilterStrategy(public val test: (provided: String, candidate: String) -> Boolean) { - /** Filter options based on whether they contain the provided value. **/ - public object Contains : FilterStrategy({ provided, candidate -> - candidate.contains(provided, true) - }) - - /** Filter options based on whether they start with the provided value. **/ - public object Prefix : FilterStrategy({ provided, candidate -> - candidate.startsWith(provided, true) - }) - - /** Filter options based on whether they end with the provided value. **/ - public object Suffix : FilterStrategy({ provided, candidate -> - candidate.endsWith(provided, true) - }) + /** Filter options based on whether they contain the provided value. **/ + public object Contains : FilterStrategy({ provided, candidate -> + candidate.contains(provided, true) + }) + + /** Filter options based on whether they start with the provided value. **/ + public object Prefix : FilterStrategy({ provided, candidate -> + candidate.startsWith(provided, true) + }) + + /** Filter options based on whether they end with the provided value. **/ + public object Suffix : FilterStrategy({ provided, candidate -> + candidate.endsWith(provided, true) + }) } /** Retrieve the option that's currently focused in the client. **/ public val AutoCompleteInteractionCreateEvent.focusedOption: OptionValue<*> - get() = this.interaction.command.options.values.first { it.focused } + get() = this.interaction.command.options.values.first { it.focused } /** Use a map to populate an autocomplete interaction, filtering as described by the provided [strategy]. **/ public suspend inline fun AutoCompleteInteraction.suggestStringMap( - map: Map, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + map: Map, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - val option = focusedOption.value as? String - var options = map - - if (option != null) { - options = options.filterKeys { strategy.test(option, it) } - } - - if (options.size > MAX_SUGGESTIONS) { - options = options.entries.sortedBy { it.key }.take(MAX_SUGGESTIONS).associate { it.toPair() } - } - - suggestString { - if (suggestInputWithoutMatches && options.isEmpty() && !option.isNullOrEmpty()) { - choice(option, option) - } else { - options.forEach(::choice) - } - } + val option = focusedOption.value as? String + var options = map + + if (option != null) { + options = options.filterKeys { strategy.test(option, it) } + } + + if (options.size > MAX_SUGGESTIONS) { + options = options.entries.sortedBy { it.key }.take(MAX_SUGGESTIONS).associate { it.toPair() } + } + + suggestString { + if (suggestInputWithoutMatches && options.isEmpty() && !option.isNullOrEmpty()) { + choice(option, option) + } else { + options.forEach(::choice) + } + } } /** @@ -73,28 +73,28 @@ public suspend inline fun AutoCompleteInteraction.suggestStringMap( * [strategy]. */ public suspend inline fun AutoCompleteInteraction.suggestStringCollection( - collection: Collection, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + collection: Collection, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - suggestStringMap( - collection.associateBy { it }, - strategy, - suggestInputWithoutMatches - ) + suggestStringMap( + collection.associateBy { it }, + strategy, + suggestInputWithoutMatches + ) } /** Use a map to populate an autocomplete interaction, filtering as described by the provided [strategy]. **/ public suspend inline fun AutoCompleteInteraction.suggestIntMap( - map: Map, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + map: Map, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - suggestLongMap( - map.mapValues { it.value.toLong() }, - strategy, - suggestInputWithoutMatches - ) + suggestLongMap( + map.mapValues { it.value.toLong() }, + strategy, + suggestInputWithoutMatches + ) } /** @@ -102,45 +102,45 @@ public suspend inline fun AutoCompleteInteraction.suggestIntMap( * [strategy]. */ public suspend inline fun AutoCompleteInteraction.suggestIntCollection( - collection: Collection, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + collection: Collection, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - suggestIntMap( - collection.associateBy { it.toString() }, - strategy, - suggestInputWithoutMatches - ) + suggestIntMap( + collection.associateBy { it.toString() }, + strategy, + suggestInputWithoutMatches + ) } /** Use a map to populate an autocomplete interaction, filtering as described by the provided [strategy]. **/ public suspend inline fun AutoCompleteInteraction.suggestLongMap( - map: Map, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + map: Map, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - val option = focusedOption.value as? String - var options = map - - if (option != null) { - options = options.filterKeys { strategy.test(option, it) } - } - - if (options.size > MAX_SUGGESTIONS) { - options = options.entries.sortedBy { it.key }.take(MAX_SUGGESTIONS).associate { it.toPair() } - } - - suggestInteger { - if (suggestInputWithoutMatches && options.isEmpty() && !option.isNullOrEmpty()) { - val longValue = option.toLongOrNull() - - if (longValue != null) { - choice(option, longValue) - } - } else { - options.forEach(::choice) - } - } + val option = focusedOption.value as? String + var options = map + + if (option != null) { + options = options.filterKeys { strategy.test(option, it) } + } + + if (options.size > MAX_SUGGESTIONS) { + options = options.entries.sortedBy { it.key }.take(MAX_SUGGESTIONS).associate { it.toPair() } + } + + suggestInteger { + if (suggestInputWithoutMatches && options.isEmpty() && !option.isNullOrEmpty()) { + val longValue = option.toLongOrNull() + + if (longValue != null) { + choice(option, longValue) + } + } else { + options.forEach(::choice) + } + } } /** @@ -148,24 +148,24 @@ public suspend inline fun AutoCompleteInteraction.suggestLongMap( * [strategy]. */ public suspend inline fun AutoCompleteInteraction.suggestLongCollection( - collection: Collection, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + collection: Collection, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - suggestLongMap( - collection.associateBy { it.toString() }, - strategy, - suggestInputWithoutMatches - ) + suggestLongMap( + collection.associateBy { it.toString() }, + strategy, + suggestInputWithoutMatches + ) } /** Use a map to populate an autocomplete interaction, filtering as described by the provided [strategy]. **/ public suspend inline fun AutoCompleteInteraction.suggestDoubleMap( - map: Map, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + map: Map, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - suggestNumberMap(map, strategy, suggestInputWithoutMatches) + suggestNumberMap(map, strategy, suggestInputWithoutMatches) } /** @@ -173,45 +173,45 @@ public suspend inline fun AutoCompleteInteraction.suggestDoubleMap( * [strategy]. */ public suspend inline fun AutoCompleteInteraction.suggestDoubleCollection( - collection: Collection, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + collection: Collection, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - suggestDoubleMap( - collection.associateBy { it.toString() }, - strategy, - suggestInputWithoutMatches - ) + suggestDoubleMap( + collection.associateBy { it.toString() }, + strategy, + suggestInputWithoutMatches + ) } /** Use a map to populate an autocomplete interaction, filtering as described by the provided [strategy]. **/ public suspend inline fun AutoCompleteInteraction.suggestNumberMap( - map: Map, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + map: Map, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - val option = focusedOption.value as? String - var options = map - - if (option != null) { - options = options.filterKeys { strategy.test(option, it) } - } - - if (options.size > MAX_SUGGESTIONS) { - options = options.entries.sortedBy { it.key }.take(MAX_SUGGESTIONS).associate { it.toPair() } - } - - suggestNumber { - if (suggestInputWithoutMatches && options.isEmpty() && !option.isNullOrEmpty()) { - val doubleValue = option.toDoubleOrNull() - - if (doubleValue != null) { - choice(option, doubleValue) - } - } else { - options.forEach(::choice) - } - } + val option = focusedOption.value as? String + var options = map + + if (option != null) { + options = options.filterKeys { strategy.test(option, it) } + } + + if (options.size > MAX_SUGGESTIONS) { + options = options.entries.sortedBy { it.key }.take(MAX_SUGGESTIONS).associate { it.toPair() } + } + + suggestNumber { + if (suggestInputWithoutMatches && options.isEmpty() && !option.isNullOrEmpty()) { + val doubleValue = option.toDoubleOrNull() + + if (doubleValue != null) { + choice(option, doubleValue) + } + } else { + options.forEach(::choice) + } + } } /** @@ -219,13 +219,13 @@ public suspend inline fun AutoCompleteInteraction.suggestNumberMap( * [strategy]. */ public suspend inline fun AutoCompleteInteraction.suggestNumberCollection( - collection: Collection, - strategy: FilterStrategy = FilterStrategy.Prefix, - suggestInputWithoutMatches: Boolean = false, + collection: Collection, + strategy: FilterStrategy = FilterStrategy.Prefix, + suggestInputWithoutMatches: Boolean = false, ) { - suggestNumberMap( - collection.associateBy { it.toString() }, - strategy, - suggestInputWithoutMatches - ) + suggestNumberMap( + collection.associateBy { it.toString() }, + strategy, + suggestInputWithoutMatches + ) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Koin.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Koin.kt index 2ba6da3481..0db2627fb5 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Koin.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Koin.kt @@ -14,14 +14,14 @@ import org.koin.dsl.module /** Wrapper for [org.koin.dsl.module] that immediately loads the module for the current [Koin] instance. **/ public fun loadModule( - createdAtStart: Boolean = false, - moduleDeclaration: ModuleDeclaration + createdAtStart: Boolean = false, + moduleDeclaration: ModuleDeclaration, ): Module { - val moduleObj = module(createdAtStart, moduleDeclaration) + val moduleObj = module(createdAtStart, moduleDeclaration) - KordExContext.loadKoinModules(moduleObj) + KordExContext.loadKoinModules(moduleObj) - return moduleObj + return moduleObj } /** Retrieve the current [Koin] instance. **/ diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Kord.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Kord.kt index d6d2335509..880e144448 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Kord.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Kord.kt @@ -21,7 +21,7 @@ import kotlin.time.Duration /** Flow containing all [User] objects in the cache. **/ public val Kord.users: Flow - get() = with(EntitySupplyStrategy.cache).users + get() = with(EntitySupplyStrategy.cache).users /** * Return the first received event that matches the condition. @@ -31,14 +31,14 @@ public val Kord.users: Flow * @param condition Function return true if the event object is valid and should be returned. */ public suspend inline fun Kord.waitFor( - timeout: Long? = null, - noinline condition: (suspend T.() -> Boolean) = { true } + timeout: Long? = null, + noinline condition: (suspend T.() -> Boolean) = { true }, ): T? = if (timeout == null) { - events.filterIsInstance().firstOrNull(condition) + events.filterIsInstance().firstOrNull(condition) } else { - withTimeoutOrNull(timeout) { - events.filterIsInstance().firstOrNull(condition) - } + withTimeoutOrNull(timeout) { + events.filterIsInstance().firstOrNull(condition) + } } /** @@ -49,14 +49,14 @@ public suspend inline fun Kord.waitFor( * @param condition Function return true if the event object is valid and should be returned. */ public suspend inline fun Kord.waitFor( - timeout: Duration? = null, - noinline condition: (suspend T.() -> Boolean) = { true } + timeout: Duration? = null, + noinline condition: (suspend T.() -> Boolean) = { true }, ): T? = if (timeout == null) { - events.filterIsInstance().firstOrNull(condition) + events.filterIsInstance().firstOrNull(condition) } else { - withTimeoutOrNull(timeout) { - events.filterIsInstance().firstOrNull(condition) - } + withTimeoutOrNull(timeout) { + events.filterIsInstance().firstOrNull(condition) + } } /** @@ -69,16 +69,16 @@ public suspend inline fun Kord.waitFor( @KordPreview @Suppress("ExpressionBodySyntax") public suspend inline fun LiveKordEntity.waitFor( - timeout: Long? = null, - noinline condition: (suspend T.() -> Boolean) = { true } + timeout: Long? = null, + noinline condition: (suspend T.() -> Boolean) = { true }, ): T? { - return if (timeout == null) { - events.filterIsInstance().firstOrNull(condition) - } else { - withTimeoutOrNull(timeout) { - events.filterIsInstance().firstOrNull(condition) - } - } + return if (timeout == null) { + events.filterIsInstance().firstOrNull(condition) + } else { + withTimeoutOrNull(timeout) { + events.filterIsInstance().firstOrNull(condition) + } + } } /** @@ -89,12 +89,12 @@ public suspend inline fun LiveKordEntity.waitFor( * @param condition Function return true if the event object is valid and should be returned. */ public suspend inline fun ExtensibleBot.waitFor( - timeout: Duration? = null, - noinline condition: (suspend T.() -> Boolean) = { true } + timeout: Duration? = null, + noinline condition: (suspend T.() -> Boolean) = { true }, ): T? = if (timeout == null) { - events.filterIsInstance().firstOrNull(condition) + events.filterIsInstance().firstOrNull(condition) } else { - withTimeoutOrNull(timeout) { - events.filterIsInstance().firstOrNull(condition) - } + withTimeoutOrNull(timeout) { + events.filterIsInstance().firstOrNull(condition) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Maps.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Maps.kt index dfc7999dd9..dd36f724de 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Maps.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Maps.kt @@ -24,8 +24,9 @@ public typealias MutableStringMap = MutableStringKeyedMap /** Provides direct access to the map KordEx registers for [Event.customContext]. **/ @OptIn(KordPreview::class) @Suppress("UNCHECKED_CAST") -public val Event.extraData: MutableStringKeyedMap get() = - customContext as MutableStringKeyedMap +public val Event.extraData: MutableStringKeyedMap + get() = + customContext as MutableStringKeyedMap /** * Utility function for getting a key from the given String-keyed map, attempting to cast it to the given generic @@ -36,8 +37,8 @@ public val Event.extraData: MutableStringKeyedMap get() = * @throws IllegalArgumentException when the map doesn't contain the given key */ public inline fun StringKeyedMap<*>.getOf(key: String): T = - (this[key] ?: throw IllegalArgumentException("Map does not contain key: $key")) - as T + (this[key] ?: throw IllegalArgumentException("Map does not contain key: $key")) + as T /** * Utility function for getting a key from the given String-keyed map, attempting to cast it to the given generic @@ -46,7 +47,7 @@ public inline fun StringKeyedMap<*>.getOf(key: String): T = * **Note:** This function does not support maps with nullable values. */ public inline fun StringKeyedMap<*>.getOfOrDefault(key: String, default: T): T = - this[key] as? T ?: default + this[key] as? T ?: default /** * Utility function for getting a key from the given String-keyed map, attempting to cast it to the given generic @@ -55,7 +56,7 @@ public inline fun StringKeyedMap<*>.getOfOrDefault(key: String * **Note:** This function does not support maps with nullable values. */ public inline fun StringKeyedMap<*>.getOfOrNull(key: String): T? = - this[key] as? T + this[key] as? T /** * Utility function for getting a key from the given String-keyed map, attempting to cast it to the given generic @@ -66,19 +67,19 @@ public inline fun StringKeyedMap<*>.getOfOrNull(key: String): * **Note:** This function does not support maps with nullable values. */ public inline fun MutableStringKeyedMap.getOfOrDefault( - key: String, - default: T, - store: Boolean, + key: String, + default: T, + store: Boolean, ): T { - val value = this[key] as? T + val value = this[key] as? T - if (value == null) { - if (store) { - this[key] = default - } + if (value == null) { + if (store) { + this[key] = default + } - return default - } + return default + } - return value + return value } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Member.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Member.kt index 26ee4b9a66..79460ad13a 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Member.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Member.kt @@ -20,14 +20,14 @@ import kotlin.time.Duration /** A more sensible name than `communicationDisabledUntil`. **/ public val Member.timeoutUntil: Instant? - inline get() = this.communicationDisabledUntil + inline get() = this.communicationDisabledUntil /** A more sensible name than `communicationDisabledUntil`. **/ public var MemberModifyBuilder.timeoutUntil: Instant? - inline get() = this.communicationDisabledUntil - inline set(value) { - this.communicationDisabledUntil = value - } + inline get() = this.communicationDisabledUntil + inline set(value) { + this.communicationDisabledUntil = value + } /** * Check if the user has the given [Role]. @@ -52,11 +52,11 @@ public fun Member.hasRoles(vararg roles: RoleBehavior): Boolean = hasRoles(roles * @return `true` if the user has all of the given roles, `false` otherwise. */ public fun Member.hasRoles(roles: Collection): Boolean = - if (roles.isEmpty()) { - true - } else { - this.roleIds.containsAll(roles.map { it.id }) - } + if (roles.isEmpty()) { + true + } else { + this.roleIds.containsAll(roles.map { it.id }) + } /** * Convenience function to retrieve a user's top [Role]. @@ -102,13 +102,13 @@ public suspend inline fun Member.hasPermissions(vararg perms: Permission): Boole * @return `true` if the collection is empty, or the [Member] has all of the given permissions, `false` otherwise */ public suspend fun Member.hasPermissions(perms: Collection): Boolean = - if (perms.isEmpty()) { - true - } else { - val permissions = getPermissions() + if (perms.isEmpty()) { + true + } else { + val permissions = getPermissions() - perms.all { it in permissions } - } + perms.all { it in permissions } + } /** * Checks if this [Member] can interact (delete/edit/assign/..) with the specified [Role]. @@ -119,12 +119,12 @@ public suspend fun Member.hasPermissions(perms: Collection): Boolean * Throws an [IllegalArgumentException] if the role is from a different guild. */ public suspend fun Member.canInteract(role: Role): Boolean { - val guild = getGuild() + val guild = getGuild() - if (guild.ownerId == this.id) return true + if (guild.ownerId == this.id) return true - val highestRole = getTopRole() ?: guild.getEveryoneRole() - return highestRole.canInteract(role) + val highestRole = getTopRole() ?: guild.getEveryoneRole() + return highestRole.canInteract(role) } /** @@ -137,15 +137,15 @@ public suspend fun Member.canInteract(role: Role): Boolean { * Throws an [IllegalArgumentException] if the member is from a different guild. */ public suspend fun Member.canInteract(member: Member): Boolean { - val guild = getGuild() + val guild = getGuild() - if (isOwner()) return true - if (member.isOwner()) return false + if (isOwner()) return true + if (member.isOwner()) return false - val highestRole = getTopRole() ?: guild.getEveryoneRole() - val otherHighestRole = member.getTopRole() ?: guild.getEveryoneRole() + val highestRole = getTopRole() ?: guild.getEveryoneRole() + val otherHighestRole = member.getTopRole() ?: guild.getEveryoneRole() - return highestRole.canInteract(otherHighestRole) + return highestRole.canInteract(otherHighestRole) } /** @@ -153,22 +153,22 @@ public suspend fun Member.canInteract(member: Member): Boolean { */ @DoNotChain public suspend fun Member.removeTimeout(reason: String? = null): Member = - edit { - timeoutUntil = null + edit { + timeoutUntil = null - this.reason = reason - } + this.reason = reason + } /** * Convenience function to time out a member using a [Duration], skipping the [edit] DSL. */ @DoNotChain public suspend fun Member.timeout(until: Duration, reason: String? = null): Member = - edit { - timeoutUntil = Clock.System.now() + until + edit { + timeoutUntil = Clock.System.now() + until - this.reason = reason - } + this.reason = reason + } /** * Convenience function to time out a member using a [DateTimePeriod] and timezone, skipping the [edit] DSL. @@ -177,24 +177,24 @@ public suspend fun Member.timeout(until: Duration, reason: String? = null): Memb */ @DoNotChain public suspend fun Member.timeout( - until: DateTimePeriod, - timezone: TimeZone = TimeZone.UTC, - reason: String? = null + until: DateTimePeriod, + timezone: TimeZone = TimeZone.UTC, + reason: String? = null, ): Member = - edit { - timeoutUntil = Clock.System.now().plus(until, timezone) + edit { + timeoutUntil = Clock.System.now().plus(until, timezone) - this.reason = reason - } + this.reason = reason + } /** * Convenience function to server mute a member, skipping the [edit] DSL. */ @DoNotChain public suspend fun Member.mute(reason: String? = null): Member = edit { - muted = true + muted = true - this.reason = reason + this.reason = reason } /** @@ -202,9 +202,9 @@ public suspend fun Member.mute(reason: String? = null): Member = edit { */ @DoNotChain public suspend fun Member.unMute(reason: String? = null): Member = edit { - muted = false + muted = false - this.reason = reason + this.reason = reason } /** @@ -212,9 +212,9 @@ public suspend fun Member.unMute(reason: String? = null): Member = edit { */ @DoNotChain public suspend fun Member.deafen(reason: String? = null): Member = edit { - deafened = true + deafened = true - this.reason = reason + this.reason = reason } /** @@ -222,9 +222,9 @@ public suspend fun Member.deafen(reason: String? = null): Member = edit { */ @DoNotChain public suspend fun Member.unDeafen(reason: String? = null): Member = edit { - deafened = false + deafened = false - this.reason = reason + this.reason = reason } /** @@ -235,9 +235,9 @@ public suspend fun Member.unDeafen(reason: String? = null): Member = edit { */ @DoNotChain public suspend fun Member.setNickname(nickname: String?, reason: String? = null): Member = edit { - this.nickname = nickname + this.nickname = nickname - this.reason = reason + this.reason = reason } /** @@ -247,4 +247,4 @@ public suspend fun Member.setNickname(nickname: String?, reason: String? = null) */ @DoNotChain public suspend fun Member.removeNickname(reason: String? = null): Member = - setNickname(null, reason) + setNickname(null, reason) diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Message.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Message.kt index 24a7ea4789..ed7829cfc5 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Message.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Message.kt @@ -24,7 +24,6 @@ import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.event.message.* import dev.kord.rest.builder.message.allowedMentions import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.create.allowedMentions import dev.kord.rest.request.RestRequestException import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.* @@ -39,26 +38,26 @@ private const val DISCORD_CHANNEL_URI = "https://discord.com/channels" * Deletes a message, catching and ignoring an HTTP 404 (Not Found) exception. */ public suspend fun MessageBehavior.deleteIgnoringNotFound() { - try { - delete() - } catch (e: RestRequestException) { - if (e.hasNotStatus(HttpStatusCode.NotFound)) { - throw e - } - } + try { + delete() + } catch (e: RestRequestException) { + if (e.hasNotStatus(HttpStatusCode.NotFound)) { + throw e + } + } } /** * Deletes a public follow-up, catching and ignoring an HTTP 404 (Not Found) exception. */ public suspend fun PublicFollowupMessageBehavior.deleteIgnoringNotFound() { - try { - delete() - } catch (e: RestRequestException) { - if (e.hasNotStatus(HttpStatusCode.NotFound)) { - throw e - } - } + try { + delete() + } catch (e: RestRequestException) { + if (e.hasNotStatus(HttpStatusCode.NotFound)) { + throw e + } + } } /** @@ -70,22 +69,22 @@ public suspend fun PublicFollowupMessageBehavior.deleteIgnoringNotFound() { * @return Job spawned by the CoroutineScope. */ public fun MessageBehavior.delete(millis: Long, retry: Boolean = true): Job { - return kord.launch { - delay(millis) - - try { - this@delete.deleteIgnoringNotFound() - } catch (e: RestRequestException) { - val message = this@delete - - if (retry) { - logger.debug(e) { "Failed to delete message, retrying: $message" } - this@delete.delete(millis, false) - } else { - logger.error(e) { "Failed to delete message: $message" } - } - } - } + return kord.launch { + delay(millis) + + try { + this@delete.deleteIgnoringNotFound() + } catch (e: RestRequestException) { + val message = this@delete + + if (retry) { + logger.debug(e) { "Failed to delete message, retrying: $message" } + this@delete.delete(millis, false) + } else { + logger.error(e) { "Failed to delete message: $message" } + } + } + } } /** @@ -97,22 +96,22 @@ public fun MessageBehavior.delete(millis: Long, retry: Boolean = true): Job { * @return Job spawned by the CoroutineScope. */ public fun PublicFollowupMessageBehavior.delete(millis: Long, retry: Boolean = true): Job { - return kord.launch { - delay(millis) - - try { - this@delete.deleteIgnoringNotFound() - } catch (e: RestRequestException) { - val message = this@delete - - if (retry) { - logger.debug(e) { "Failed to delete message, retrying: $message" } - this@delete.delete(millis, false) - } else { - logger.error(e) { "Failed to delete message: $message" } - } - } - } + return kord.launch { + delay(millis) + + try { + this@delete.deleteIgnoringNotFound() + } catch (e: RestRequestException) { + val message = this@delete + + if (retry) { + logger.debug(e) { "Failed to delete message, retrying: $message" } + this@delete.delete(millis, false) + } else { + logger.error(e) { "Failed to delete message: $message" } + } + } + } } /** @@ -128,7 +127,7 @@ public suspend inline fun MessageBehavior.addReaction(emoji: String): Unit = add * @param emoji Emoji to remove from the message. */ public suspend inline fun MessageBehavior.deleteReaction(userId: Snowflake, emoji: GuildEmoji): Unit = - deleteReaction(userId, emoji.toReaction()) + deleteReaction(userId, emoji.toReaction()) /** * Remove a reaction from this message, using the Unicode emoji represented by the given string. @@ -136,7 +135,7 @@ public suspend inline fun MessageBehavior.deleteReaction(userId: Snowflake, emoj * @param emoji Emoji to remove from message. */ public suspend inline fun MessageBehavior.deleteReaction(userId: Snowflake, emoji: String): Unit = - deleteReaction(userId, emoji.toReaction()) + deleteReaction(userId, emoji.toReaction()) /** * Remove a reaction from this message, using a guild's custom emoji object. @@ -158,7 +157,7 @@ public suspend inline fun MessageBehavior.deleteReaction(unicode: String): Unit * @param emoji Emoji to remove from the message. */ public suspend inline fun MessageBehavior.deleteOwnReaction(emoji: GuildEmoji): Unit = - deleteOwnReaction(emoji.toReaction()) + deleteOwnReaction(emoji.toReaction()) /** * Remove a reaction from this message belonging to the bot, using the Unicode emoji represented by the given string. @@ -166,15 +165,15 @@ public suspend inline fun MessageBehavior.deleteOwnReaction(emoji: GuildEmoji): * @param unicode Emoji to remove from the message. */ public suspend inline fun MessageBehavior.deleteOwnReaction(unicode: String): Unit = - deleteOwnReaction(unicode.toReaction()) + deleteOwnReaction(unicode.toReaction()) /** Message author's ID. **/ public val MessageData.authorId: Snowflake - get() = author.id + get() = author.id /** Whether the message author is a bot. **/ public val MessageData.authorIsBot: Boolean - get() = author.bot.discordBoolean + get() = author.bot.discordBoolean /** * Respond to a message in the channel it was sent to, mentioning the author. @@ -187,7 +186,7 @@ public val MessageData.authorIsBot: Boolean * @return The newly-created response message. */ public suspend fun Message.respond(content: String, useReply: Boolean = true, pingInReply: Boolean = true): Message = - respond(useReply, pingInReply) { this.content = content } + respond(useReply, pingInReply) { this.content = content } /** * Respond to a message in the channel it was sent to, mentioning the author. @@ -200,39 +199,39 @@ public suspend fun Message.respond(content: String, useReply: Boolean = true, pi * @return The newly-created response message. */ public suspend fun Message.respond( - useReply: Boolean = true, - pingInReply: Boolean = true, - builder: suspend MessageCreateBuilder.() -> Unit + useReply: Boolean = true, + pingInReply: Boolean = true, + builder: suspend MessageCreateBuilder.() -> Unit, ): Message { - val author = this.author - val innerBuilder: suspend MessageCreateBuilder.() -> Unit = { - builder() - - allowedMentions { - when { - useReply && pingInReply -> repliedUser = true - author != null && !pingInReply -> users.add(author.id) - } - } - - val mention = if (author != null && !useReply && getChannelOrNull() !is DmChannel) { - author.mention - } else { - "" - } - - val contentWithMention = "$mention ${content ?: ""}".trim() - - if (contentWithMention.isNotEmpty()) { - content = contentWithMention - } - } - - return if (useReply) { - reply { innerBuilder() } - } else { - channel.createMessage { innerBuilder() } - } + val author = this.author + val innerBuilder: suspend MessageCreateBuilder.() -> Unit = { + builder() + + allowedMentions { + when { + useReply && pingInReply -> repliedUser = true + author != null && !pingInReply -> users.add(author.id) + } + } + + val mention = if (author != null && !useReply && getChannelOrNull() !is DmChannel) { + author.mention + } else { + "" + } + + val contentWithMention = "$mention ${content ?: ""}".trim() + + if (contentWithMention.isNotEmpty()) { + content = contentWithMention + } + } + + return if (useReply) { + reply { innerBuilder() } + } else { + channel.createMessage { innerBuilder() } + } } /** @@ -241,7 +240,7 @@ public suspend fun Message.respond( * @return A clickable URL to jump to this message. */ public fun Message.getJumpUrl(): String = - "$DISCORD_CHANNEL_URI/${data.guildId.value?.value ?: "@me"}/${channelId.value}/${id.value}" + "$DISCORD_CHANNEL_URI/${data.guildId.value?.value ?: "@me"}/${channelId.value}/${id.value}" /** * Generate the jump URL for this message. @@ -249,7 +248,7 @@ public fun Message.getJumpUrl(): String = * @return A clickable URL to jump to this message. */ public fun DiscordPartialMessage.getJumpUrl(): String = - "$DISCORD_CHANNEL_URI/${guildId.value?.value ?: "@me"}/${channelId.value}/${id.value}" + "$DISCORD_CHANNEL_URI/${guildId.value?.value ?: "@me"}/${channelId.value}/${id.value}" /** * Check that this message happened in either the given channel or a DM, or that the author is at least a given role. @@ -267,39 +266,39 @@ public fun DiscordPartialMessage.getJumpUrl(): String = * @return true if the message was posted in an appropriate context, false otherwise */ public suspend fun Message.requireChannel( - context: CommandContext, - channel: GuildMessageChannel, - role: Role? = null, - delay: Long = DELETE_DELAY, - allowDm: Boolean = true, - deleteOriginal: Boolean = true, - deleteResponse: Boolean = true + context: CommandContext, + channel: GuildMessageChannel, + role: Role? = null, + delay: Long = DELETE_DELAY, + allowDm: Boolean = true, + deleteOriginal: Boolean = true, + deleteResponse: Boolean = true, ): Boolean { - val topRole = if (getGuildOrNull() == null) { - null - } else { - getAuthorAsMemberOrNull()?.getTopRole() - } - - val messageChannel = getChannelOrNull() - - @Suppress("UnnecessaryParentheses") // In this case, it feels more readable - if ( - (allowDm && messageChannel is DmChannel) || - (role != null && topRole != null && topRole >= role) || - channelId == channel.id - ) { - return true - } - - val response = respond( - context.translate("utils.message.useThisChannel", replacements = arrayOf(channel.mention)) - ) - - if (deleteResponse) response.delete(delay) - if (deleteOriginal && messageChannel !is DmChannel) this.delete(delay) - - return false + val topRole = if (getGuildOrNull() == null) { + null + } else { + getAuthorAsMemberOrNull()?.getTopRole() + } + + val messageChannel = getChannelOrNull() + + @Suppress("UnnecessaryParentheses") // In this case, it feels more readable + if ( + (allowDm && messageChannel is DmChannel) || + (role != null && topRole != null && topRole >= role) || + channelId == channel.id + ) { + return true + } + + val response = respond( + context.translate("utils.message.useThisChannel", replacements = arrayOf(channel.mention)) + ) + + if (deleteResponse) response.delete(delay) + if (deleteOriginal && messageChannel !is DmChannel) this.delete(delay) + + return false } /** @@ -313,28 +312,28 @@ public suspend fun Message.requireChannel( * @return true if the message was posted in an appropriate context, false otherwise */ public suspend fun Message.requireGuildChannel( - context: CommandContext, - role: Role? = null + context: CommandContext, + role: Role? = null, ): Boolean { - val author = this.author - val guild = getGuildOrNull() - - val topRole = if (author != null && guild != null) { - author.asMemberOrNull(guild.id) - } else { - null - } - - @Suppress("UnnecessaryParentheses") // In this case, it feels more readable - if ( - (role != null && topRole != null && topRole >= role) || - getChannelOrNull() !is DmChannel - ) { - return true - } - - respond(context.translate("utils.message.commandNotAvailableInDm")) - return false + val author = this.author + val guild = getGuildOrNull() + + val topRole = if (author != null && guild != null) { + author.asMemberOrNull(guild.id) + } else { + null + } + + @Suppress("UnnecessaryParentheses") // In this case, it feels more readable + if ( + (role != null && topRole != null && topRole >= role) || + getChannelOrNull() !is DmChannel + ) { + return true + } + + respond(context.translate("utils.message.commandNotAvailableInDm")) + return false } /** @@ -352,58 +351,58 @@ public suspend fun Message.requireGuildChannel( * @return true if the message was posted in an appropriate context, false otherwise */ public suspend fun Message.requireGuildChannel( - context: CommandContext, - role: Role? = null, - guild: Guild? = null + context: CommandContext, + role: Role? = null, + guild: Guild? = null, ): Boolean { - val author = this.author - val topRole = if (author != null) { - guild?.getMember(author.id)?.getTopRole() - } else { - null - } - - @Suppress("UnnecessaryParentheses") // In this case, it feels more readable - if ( - (role != null && topRole != null && topRole >= role) || - getChannelOrNull() !is DmChannel - ) { - return true - } - - respond(context.translate("utils.message.commandNotAvailableInDm")) - return false + val author = this.author + val topRole = if (author != null) { + guild?.getMember(author.id)?.getTopRole() + } else { + null + } + + @Suppress("UnnecessaryParentheses") // In this case, it feels more readable + if ( + (role != null && topRole != null && topRole >= role) || + getChannelOrNull() !is DmChannel + ) { + return true + } + + respond(context.translate("utils.message.commandNotAvailableInDm")) + return false } /** Whether this message was published to the guilds that are following its channel. **/ public val Message.isPublished: Boolean - get() = - data.flags.value?.contains(MessageFlag.CrossPosted) == true + get() = + data.flags.value?.contains(MessageFlag.CrossPosted) == true /** Whether this message was sent from a different guild's followed announcement channel. **/ public val Message.isCrossPost: Boolean - get() = - data.flags.value?.contains(MessageFlag.IsCrossPost) == true + get() = + data.flags.value?.contains(MessageFlag.IsCrossPost) == true /** Whether this message's embeds should be serialized. **/ public val Message.suppressEmbeds: Boolean - get() = - data.flags.value?.contains(MessageFlag.SuppressEmbeds) == true + get() = + data.flags.value?.contains(MessageFlag.SuppressEmbeds) == true /** When [isCrossPost], whether the source message has been deleted from the original guild. **/ public val Message.originalMessageDeleted: Boolean - get() = - data.flags.value?.contains(MessageFlag.SourceMessageDeleted) == true + get() = + data.flags.value?.contains(MessageFlag.SourceMessageDeleted) == true /** Whether this message came from Discord's urgent message system. **/ public val Message.isUrgent: Boolean - get() = - data.flags.value?.contains(MessageFlag.Urgent) == true + get() = + data.flags.value?.contains(MessageFlag.Urgent) == true /** Whether this is an ephemeral message from the Interactions system. **/ public val Message.isEphemeral: Boolean - get() = - data.flags.value?.contains(MessageFlag.Ephemeral) == true + get() = + data.flags.value?.contains(MessageFlag.Ephemeral) == true /** * Wait for a message, using the given timeout (in milliseconds ) and filter function. @@ -411,13 +410,13 @@ public val Message.isEphemeral: Boolean * Will return `null` if no message is found before the timeout. */ public suspend fun waitForMessage( - timeout: Long, - filter: (suspend (MessageCreateEvent).() -> Boolean) = { true } + timeout: Long, + filter: (suspend (MessageCreateEvent).() -> Boolean) = { true }, ): Message? { - val kord = getKoin().get() - val event = kord.waitFor(timeout, filter) + val kord = getKoin().get() + val event = kord.waitFor(timeout, filter) - return event?.message + return event?.message } /** @@ -426,16 +425,16 @@ public suspend fun waitForMessage( * Will return `null` if no message is found before the timeout. */ public suspend fun UserBehavior.waitForMessage( - timeout: Long, - filter: (suspend (MessageCreateEvent).() -> Boolean) = { true } + timeout: Long, + filter: (suspend (MessageCreateEvent).() -> Boolean) = { true }, ): Message? { - val kord = getKoin().get() - val event = kord.waitFor(timeout) { - message.author?.id == id && - filter() - } + val kord = getKoin().get() + val event = kord.waitFor(timeout) { + message.author?.id == id && + filter() + } - return event?.message + return event?.message } /** @@ -444,16 +443,16 @@ public suspend fun UserBehavior.waitForMessage( * Will return `null` if no message is found before the timeout. */ public suspend fun MessageChannelBehavior.waitForMessage( - timeout: Long, - filter: (suspend (MessageCreateEvent).() -> Boolean) = { true } + timeout: Long, + filter: (suspend (MessageCreateEvent).() -> Boolean) = { true }, ): Message? { - val kord = getKoin().get() - val event = kord.waitFor(timeout) { - message.channelId == id && - filter() - } + val kord = getKoin().get() + val event = kord.waitFor(timeout) { + message.channelId == id && + filter() + } - return event?.message + return event?.message } /** @@ -462,16 +461,16 @@ public suspend fun MessageChannelBehavior.waitForMessage( * Will return `null` if no message is found before the timeout. */ public suspend fun MessageBehavior.waitForReply( - timeout: Long, - filter: (suspend (MessageCreateEvent).() -> Boolean) = { true } + timeout: Long, + filter: (suspend (MessageCreateEvent).() -> Boolean) = { true }, ): Message? { - val kord = getKoin().get() - val event = kord.waitFor(timeout) { - message.messageReference?.message?.id == id && - filter() - } + val kord = getKoin().get() + val event = kord.waitFor(timeout) { + message.messageReference?.message?.id == id && + filter() + } - return event?.message + return event?.message } /** @@ -481,15 +480,15 @@ public suspend fun MessageBehavior.waitForReply( * Will return `null` if no message is found before the timeout. */ public suspend fun CommandContext.waitForResponse( - timeout: Long, - filter: (suspend (MessageCreateEvent).() -> Boolean) = { true } + timeout: Long, + filter: (suspend (MessageCreateEvent).() -> Boolean) = { true }, ): Message? { - val kord = com.kotlindiscord.kord.extensions.utils.getKoin().get() - val event = kord.waitFor(timeout) { - message.author?.id == getUser()?.id && - message.channelId == getChannel().id && - filter() - } - - return event?.message + val kord = com.kotlindiscord.kord.extensions.utils.getKoin().get() + val event = kord.waitFor(timeout) { + message.author?.id == getUser()?.id && + message.channelId == getChannel().id && + filter() + } + + return event?.message } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Misc.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Misc.kt index d0b5f34434..8972e68658 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Misc.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Misc.kt @@ -23,13 +23,13 @@ import kotlin.time.Duration * Simple convenience function for mapping from `0` to the given [Int], exclusively. */ public inline fun Int.map(body: (Int) -> T): List = - (0 until this).map { body(it) } + (0 until this).map { body(it) } /** * Simple convenience function for mapping from `0` to the given [Long], exclusively. */ public inline fun Long.map(body: (Long) -> T): List = - (0 until this).map { body(it) } + (0 until this).map { body(it) } /** * Run a block of code within a coroutine scope, defined by a given dispatcher. @@ -41,29 +41,29 @@ public inline fun Long.map(body: (Long) -> T): List = * @param body The block of code to be run. */ public suspend fun runSuspended( - dispatcher: CoroutineDispatcher = Dispatchers.IO, - body: suspend CoroutineScope.() -> T + dispatcher: CoroutineDispatcher = Dispatchers.IO, + body: suspend CoroutineScope.() -> T, ): T = withContext(dispatcher, body) /** Retrieve the text from the footer of an embed builder, or `null` if no text was set. **/ public fun EmbedBuilder.Footer.textOrNull(): String? = - try { - text - } catch (e: UninitializedPropertyAccessException) { - null - } + try { + text + } catch (e: UninitializedPropertyAccessException) { + null + } /** * Returns `true` if any element in the `Flow` matches the given predicate. Consumes the `Flow`. */ public suspend inline fun Flow.any(crossinline predicate: suspend (T) -> Boolean): Boolean = - firstOrNull { predicate(it) } != null + firstOrNull { predicate(it) } != null /** * Convert the given [DateTimePeriod] to a [Duration] based on the given timezone, relative to the current system time. */ public fun DateTimePeriod.toDuration(timezone: TimeZone): Duration { - val now = Clock.System.now() + val now = Clock.System.now() - return now.plus(this, timezone) - now + return now.plus(this, timezone) - now } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_NsfwLevels.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_NsfwLevels.kt index 3151817a89..8db5c5e010 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_NsfwLevels.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_NsfwLevels.kt @@ -16,56 +16,56 @@ import java.util.* * based on severity. Nonetheless, bots will want this, so we supply sorting ordinals here.. */ public val NsfwLevel.ordinal: Int - get() = when (this) { - NsfwLevel.Safe -> -1 - NsfwLevel.Default -> 0 - NsfwLevel.AgeRestricted -> 1 - NsfwLevel.Explicit -> 2 + get() = when (this) { + NsfwLevel.Safe -> -1 + NsfwLevel.Default -> 0 + NsfwLevel.AgeRestricted -> 1 + NsfwLevel.Explicit -> 2 - is NsfwLevel.Unknown -> value - } + is NsfwLevel.Unknown -> value + } /** * Use the corresponding [ordinal] value to compare two NSFW levels' severity. */ public operator fun NsfwLevel.compareTo(other: NsfwLevel): Int = - ordinal.compareTo(other.ordinal) + ordinal.compareTo(other.ordinal) /** Given a [NsfwLevel], return a string representing its translation key. **/ public fun NsfwLevel.toTranslationKey(): String? = when (this) { - NsfwLevel.AgeRestricted -> "nsfwLevel.ageRestricted" - NsfwLevel.Default -> "nsfwLevel.default" - NsfwLevel.Explicit -> "nsfwLevel.explicit" - NsfwLevel.Safe -> "nsfwLevel.safe" + NsfwLevel.AgeRestricted -> "nsfwLevel.ageRestricted" + NsfwLevel.Default -> "nsfwLevel.default" + NsfwLevel.Explicit -> "nsfwLevel.explicit" + NsfwLevel.Safe -> "nsfwLevel.safe" - is NsfwLevel.Unknown -> null + is NsfwLevel.Unknown -> null } /** Given a [CommandContext], translate the [NsfwLevel] to a human-readable string based on the context's locale. **/ public suspend fun NsfwLevel.translate(context: CommandContext): String { - val key = toTranslationKey() + val key = toTranslationKey() - return if (key == null) { - context.translate( - "nsfwLevel.unknown", - replacements = arrayOf(value) - ) - } else { - context.translate(key) - } + return if (key == null) { + context.translate( + "nsfwLevel.unknown", + replacements = arrayOf(value) + ) + } else { + context.translate(key) + } } /** Given a locale, translate the [NsfwLevel] to a human-readable string. **/ public fun NsfwLevel.translate(locale: Locale): String { - val key = toTranslationKey() + val key = toTranslationKey() - return if (key == null) { - getKoin().get().translate( - "nsfwLevel.unknown", - locale, - replacements = arrayOf(value) - ) - } else { - getKoin().get().translate(key, locale) - } + return if (key == null) { + getKoin().get().translate( + "nsfwLevel.unknown", + locale, + replacements = arrayOf(value) + ) + } else { + getKoin().get().translate(key, locale) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Permissions.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Permissions.kt index 5c538f8e21..ab06b7f2f9 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Permissions.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Permissions.kt @@ -15,85 +15,85 @@ import java.util.* public fun Permission.toTranslationKey(): String? = when (this) { Permission.CreateEvents -> "permission.createEvents" Permission.CreateGuildExpressions -> "permission.createGuildExpressions" - Permission.AddReactions -> "permission.addReactions" - Permission.Administrator -> "permission.administrator" - Permission.AttachFiles -> "permission.attachFiles" - Permission.BanMembers -> "permission.banMembers" - Permission.ChangeNickname -> "permission.changeNickname" - Permission.Connect -> "permission.connect" - Permission.CreateInstantInvite -> "permission.createInstantInvite" - Permission.CreatePrivateThreads -> "permission.createPrivateThreads" - Permission.CreatePublicThreads -> "permission.createPublicThreads" - Permission.DeafenMembers -> "permission.deafenMembers" - Permission.EmbedLinks -> "permission.embedLinks" - Permission.KickMembers -> "permission.kickMembers" - Permission.ManageChannels -> "permission.manageChannels" - Permission.ManageEvents -> "permission.manageEvents" - Permission.ManageGuild -> "permission.manageGuild" - Permission.ManageGuildExpressions -> "permission.manageExpressions" - Permission.ManageMessages -> "permission.manageMessages" - Permission.ManageNicknames -> "permission.manageNicknames" - Permission.ManageRoles -> "permission.manageRoles" - Permission.ManageThreads -> "permission.manageThreads" - Permission.ManageWebhooks -> "permission.manageWebhooks" - Permission.MentionEveryone -> "permission.mentionEveryone" - Permission.ModerateMembers -> "permission.timeoutMembers" - Permission.MoveMembers -> "permission.moveMembers" - Permission.MuteMembers -> "permission.muteMembers" - Permission.PrioritySpeaker -> "permission.prioritySpeaker" - Permission.ReadMessageHistory -> "permission.readMessageHistory" - Permission.RequestToSpeak -> "permission.requestToSpeak" - Permission.SendMessages -> "permission.sendMessages" - Permission.SendMessagesInThreads -> "permission.sendMessagesInThreads" - Permission.SendTTSMessages -> "permission.sendTTSMessages" - Permission.SendVoiceMessages -> "permission.sendVoiceMessages" - Permission.Speak -> "permission.speak" - Permission.Stream -> "permission.stream" - Permission.UseApplicationCommands -> "permission.useApplicationCommands" - Permission.UseEmbeddedActivities -> "permission.useEmbeddedActivities" - Permission.UseExternalEmojis -> "permission.useExternalEmojis" - Permission.UseExternalSounds -> "permission.useExternalSounds" - Permission.UseExternalStickers -> "permission.useExternalStickers" - Permission.UseSoundboard -> "permission.useSoundboard" - Permission.UseVAD -> "permission.useVAD" - Permission.ViewAuditLog -> "permission.viewAuditLog" - Permission.ViewChannel -> "permission.viewChannel" - Permission.ViewCreatorMonetizationAnalytics -> "permission.viewCreatorMonetizationAnalytics" - Permission.ViewGuildInsights -> "permission.viewGuildInsights" + Permission.AddReactions -> "permission.addReactions" + Permission.Administrator -> "permission.administrator" + Permission.AttachFiles -> "permission.attachFiles" + Permission.BanMembers -> "permission.banMembers" + Permission.ChangeNickname -> "permission.changeNickname" + Permission.Connect -> "permission.connect" + Permission.CreateInstantInvite -> "permission.createInstantInvite" + Permission.CreatePrivateThreads -> "permission.createPrivateThreads" + Permission.CreatePublicThreads -> "permission.createPublicThreads" + Permission.DeafenMembers -> "permission.deafenMembers" + Permission.EmbedLinks -> "permission.embedLinks" + Permission.KickMembers -> "permission.kickMembers" + Permission.ManageChannels -> "permission.manageChannels" + Permission.ManageEvents -> "permission.manageEvents" + Permission.ManageGuild -> "permission.manageGuild" + Permission.ManageGuildExpressions -> "permission.manageExpressions" + Permission.ManageMessages -> "permission.manageMessages" + Permission.ManageNicknames -> "permission.manageNicknames" + Permission.ManageRoles -> "permission.manageRoles" + Permission.ManageThreads -> "permission.manageThreads" + Permission.ManageWebhooks -> "permission.manageWebhooks" + Permission.MentionEveryone -> "permission.mentionEveryone" + Permission.ModerateMembers -> "permission.timeoutMembers" + Permission.MoveMembers -> "permission.moveMembers" + Permission.MuteMembers -> "permission.muteMembers" + Permission.PrioritySpeaker -> "permission.prioritySpeaker" + Permission.ReadMessageHistory -> "permission.readMessageHistory" + Permission.RequestToSpeak -> "permission.requestToSpeak" + Permission.SendMessages -> "permission.sendMessages" + Permission.SendMessagesInThreads -> "permission.sendMessagesInThreads" + Permission.SendTTSMessages -> "permission.sendTTSMessages" + Permission.SendVoiceMessages -> "permission.sendVoiceMessages" + Permission.Speak -> "permission.speak" + Permission.Stream -> "permission.stream" + Permission.UseApplicationCommands -> "permission.useApplicationCommands" + Permission.UseEmbeddedActivities -> "permission.useEmbeddedActivities" + Permission.UseExternalEmojis -> "permission.useExternalEmojis" + Permission.UseExternalSounds -> "permission.useExternalSounds" + Permission.UseExternalStickers -> "permission.useExternalStickers" + Permission.UseSoundboard -> "permission.useSoundboard" + Permission.UseVAD -> "permission.useVAD" + Permission.ViewAuditLog -> "permission.viewAuditLog" + Permission.ViewChannel -> "permission.viewChannel" + Permission.ViewCreatorMonetizationAnalytics -> "permission.viewCreatorMonetizationAnalytics" + Permission.ViewGuildInsights -> "permission.viewGuildInsights" - is Permission.Unknown -> null + is Permission.Unknown -> null } /** Because "Stream" is a confusing name, people may look for "Video" instead. **/ public val Permission.Companion.Video: Permission.Stream - inline get() = Permission.Stream + inline get() = Permission.Stream /** Because it hasn't been called "Moderate Members" since the DMD testing finished. **/ public val Permission.Companion.TimeoutMembers: Permission.ModerateMembers - inline get() = Permission.ModerateMembers + inline get() = Permission.ModerateMembers /** Given a [CommandContext], translate the [Permission] to a human-readable string based on the context's locale. **/ public suspend fun Permission.translate(context: CommandContext): String { - val key = toTranslationKey() + val key = toTranslationKey() - return if (key == null) { - context.translate("permission.unknown", replacements = arrayOf(code.value)) - } else { - context.translate(key) - } + return if (key == null) { + context.translate("permission.unknown", replacements = arrayOf(code.value)) + } else { + context.translate(key) + } } /** Given a locale, translate the [Permission] to a human-readable string. **/ public fun Permission.translate(locale: Locale): String { - val key = toTranslationKey() + val key = toTranslationKey() - return if (key == null) { - getKoin().get().translate( - "permission.unknown", - locale, - replacements = arrayOf(code.value) - ) - } else { - getKoin().get().translate(key, locale) - } + return if (key == null) { + getKoin().get().translate( + "permission.unknown", + locale, + replacements = arrayOf(code.value) + ) + } else { + getKoin().get().translate(key, locale) + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_RestRequest.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_RestRequest.kt index 2ebec522fc..7b6eae24e5 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_RestRequest.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_RestRequest.kt @@ -18,11 +18,11 @@ import io.ktor.http.* * @return `true` if at least one status code matches, `false` otherwise. */ public fun RestRequestException.hasStatus(vararg codes: HttpStatusCode): Boolean { - if (codes.isEmpty()) return false + if (codes.isEmpty()) return false - val code = this.status.code + val code = this.status.code - return codes.any { it.value == code } + return codes.any { it.value == code } } /** @@ -34,11 +34,11 @@ public fun RestRequestException.hasStatus(vararg codes: HttpStatusCode): Boolean * @return `true` if at least one status code matches, `false` otherwise. */ public fun RestRequestException.hasStatusCode(vararg codes: Int): Boolean { - if (codes.isEmpty()) return false + if (codes.isEmpty()) return false - val code = this.status.code + val code = this.status.code - return codes.any { it == code } + return codes.any { it == code } } /** diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Role.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Role.kt index 2e732bfb2e..b51e241efb 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Role.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Role.kt @@ -14,9 +14,9 @@ import dev.kord.core.entity.Role * Throws an [IllegalArgumentException] when the roles are not from the same guild. */ public fun Role.canInteract(role: Role): Boolean { - if (role.guildId != guildId) { - throw IllegalArgumentException("canInteract can only be called within the same guild!") - } + if (role.guildId != guildId) { + throw IllegalArgumentException("canInteract can only be called within the same guild!") + } - return role.rawPosition < rawPosition + return role.rawPosition < rawPosition } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_String.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_String.kt index 65c9956372..309ce1f0ab 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_String.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_String.kt @@ -19,13 +19,13 @@ import java.util.* * @return Pair containing the split string. */ public fun String.splitOn(separator: (Char) -> Boolean): Pair { - val i = this.indexOfFirst(separator) + val i = this.indexOfFirst(separator) - if (i == -1) { - return this to "" - } + if (i == -1) { + return this to "" + } - return slice(IntRange(0, i - 1)) to slice(IntRange(i, this.length - 1)) + return slice(IntRange(0, i - 1)) to slice(IntRange(i, this.length - 1)) } /** @@ -57,13 +57,13 @@ public fun String.parseBoolean(locale: Locale): Boolean? = BooleanParser.parse(t * provided. */ public fun String.capitalizeWords(locale: Locale? = null): String { - return split(" ").joinToString(" ") { word -> - word.replaceFirstChar { - if (it.isLowerCase()) { - it.titlecase(locale ?: Locale.getDefault()) - } else { - it.toString() - } - } - } + return split(" ").joinToString(" ") { word -> + word.replaceFirstChar { + if (it.isLowerCase()) { + it.titlecase(locale ?: Locale.getDefault()) + } else { + it.toString() + } + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Translation.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Translation.kt index da94847612..9897bf38c4 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Translation.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Translation.kt @@ -17,58 +17,58 @@ internal val localeCache: WeakHashMap = WeakHashMap() /** Attempt to resolve the locale for the given [MessageCreateEvent] object. **/ public suspend fun MessageCreateEvent.getLocale(): Locale { - val existing = localeCache[this] + val existing = localeCache[this] - if (existing != null) { - return existing - } + if (existing != null) { + return existing + } - val bot = getKoin().get() - var result = bot.settings.i18nBuilder.defaultLocale + val bot = getKoin().get() + var result = bot.settings.i18nBuilder.defaultLocale - for (resolver in bot.settings.i18nBuilder.localeResolvers) { - val resolved = resolver(getGuildOrNull(), message.channel, message.author, null) + for (resolver in bot.settings.i18nBuilder.localeResolvers) { + val resolved = resolver(getGuildOrNull(), message.channel, message.author, null) - if (resolved != null) { - result = resolved - break - } - } + if (resolved != null) { + result = resolved + break + } + } - localeCache[this] = result + localeCache[this] = result - return result + return result } /** Attempt to resolve the locale for the given [InteractionCreateEvent] object. **/ public suspend fun InteractionCreateEvent.getLocale(): Locale { - val existing = localeCache[this] + val existing = localeCache[this] - if (existing != null) { - return existing - } + if (existing != null) { + return existing + } - val bot = getKoin().get() - var result = bot.settings.i18nBuilder.defaultLocale + val bot = getKoin().get() + var result = bot.settings.i18nBuilder.defaultLocale - for (resolver in bot.settings.i18nBuilder.localeResolvers) { - val channel = interaction.channel.asChannel() + for (resolver in bot.settings.i18nBuilder.localeResolvers) { + val channel = interaction.channel.asChannel() - val guild = if (channel is GuildChannel) { - channel.guild - } else { - null - } + val guild = if (channel is GuildChannel) { + channel.guild + } else { + null + } - val resolved = resolver(guild, interaction.channel, interaction.user, interaction) + val resolved = resolver(guild, interaction.channel, interaction.user, interaction) - if (resolved != null) { - result = resolved - break - } - } + if (resolved != null) { + result = resolved + break + } + } - localeCache[this] = result + localeCache[this] = result - return result + return result } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Users.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Users.kt index e082017b1a..0ea6cd20aa 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Users.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/_Users.kt @@ -26,31 +26,31 @@ private const val DISCORD_USERS_URI = "https://discord.com/users" * @return User's tag or username, depending on whether they've migrated or not. */ @Deprecated( - "As it appears that bots will keep their discriminators, their use is no longer deprecated. Instead, " + + "As it appears that bots will keep their discriminators, their use is no longer deprecated. Instead, " + "use the [tag] property, which will return only the username if the user doesn't have a discriminator, or " + "`user#discriminator` if they do.", level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("tag") + replaceWith = ReplaceWith("tag") ) public fun User.tagOrUsername(): String = - if (discriminator == "0") { - username - } else { - tag - } + if (discriminator == "0") { + username + } else { + tag + } /** * The user's Discord profile URL. */ public val User.profileLink: String - get() = "$DISCORD_USERS_URI/$id/" + get() = "$DISCORD_USERS_URI/$id/" /** * The user's creation timestamp. */ public val User.createdAt: Instant - get() = this.id.timestamp + get() = this.id.timestamp /** * Send a private message to a user, if they have their DMs enabled. @@ -59,15 +59,15 @@ public val User.createdAt: Instant * @return The sent message, or `null` if the user has their DMs disabled. */ public suspend inline fun User.dm(builder: MessageCreateBuilder.() -> Unit): Message? { - return try { - this.getDmChannel().createMessage { builder() } - } catch (e: RestRequestException) { - if (e.hasStatus(HttpStatusCode.Forbidden)) { - null - } else { - throw e - } - } + return try { + this.getDmChannel().createMessage { builder() } + } catch (e: RestRequestException) { + if (e.hasStatus(HttpStatusCode.Forbidden)) { + null + } else { + throw e + } + } } /** @@ -94,9 +94,9 @@ public fun topRole(guildID: Snowflake): suspend (User) -> Role? = { it.asMemberO * @return `true` if the user is `null` or a bot, `false` otherwise. */ public fun User?.isNullOrBot(): Boolean { - contract { - returns(false) implies (this@isNullOrBot !== null) - } + contract { + returns(false) implies (this@isNullOrBot !== null) + } - return this == null || isBot + return this == null || isBot } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/ChangeSet.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/ChangeSet.kt index f65d12a6b1..d4d28e2b76 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/ChangeSet.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/ChangeSet.kt @@ -14,145 +14,145 @@ import kotlin.reflect.KClass import kotlin.reflect.KProperty public class ChangeSet(public val clazz: KClass<*>) { - private val changes: MutableMap, Change<*>> = mutableMapOf() - - @Suppress("UNCHECKED_CAST") - public operator fun <@OnlyInputTypes T : Any?> get(key: KProperty): Change = - changes[key] as? Change? - ?: throw NoSuchElementException("No such element: $key") - - public operator fun <@OnlyInputTypes T : Any?> set( - key: KProperty, - value: Change, - ) { - changes[key] = value - } - - public data class Change( - public val old: Optional, - public val new: T, - - public val oldState: State, - public val newState: State, - ) - - public sealed interface State { - public object Missing : State - public object Present : State - } + private val changes: MutableMap, Change<*>> = mutableMapOf() + + @Suppress("UNCHECKED_CAST") + public operator fun <@OnlyInputTypes T : Any?> get(key: KProperty): Change = + changes[key] as? Change? + ?: throw NoSuchElementException("No such element: $key") + + public operator fun <@OnlyInputTypes T : Any?> set( + key: KProperty, + value: Change, + ) { + changes[key] = value + } + + public data class Change( + public val old: Optional, + public val new: T, + + public val oldState: State, + public val newState: State, + ) + + public sealed interface State { + public object Missing : State + public object Present : State + } } private fun valueFor(obj: Any?, value: T?): Optional = - if (obj == null) { - Optional.Missing.invoke() - } else { - value.optional() - } + if (obj == null) { + Optional.Missing.invoke() + } else { + value.optional() + } public fun VoiceState?.compare(other: VoiceState): ChangeSet { - val changeSet = ChangeSet(VoiceState::class) - - val oldState = if (this == null) { - ChangeSet.State.Missing - } else { - ChangeSet.State.Present - } - - changeSet[VoiceState::guildId] = - ChangeSet.Change( - valueFor(this, this?.guildId), - other.guildId, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::channelId] = - ChangeSet.Change( - valueFor(this, this?.channelId), - other.channelId, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::userId] = - ChangeSet.Change( - valueFor(this, this?.userId), - other.userId, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::sessionId] = - ChangeSet.Change( - valueFor(this, this?.sessionId), - other.sessionId, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::isDeafened] = - ChangeSet.Change( - valueFor(this, this?.isDeafened), - other.isDeafened, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::isMuted] = - ChangeSet.Change( - valueFor(this, this?.isMuted), - other.isMuted, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::isSelfDeafened] = - ChangeSet.Change( - valueFor(this, this?.isSelfDeafened), - other.isSelfDeafened, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::isSelfMuted] = - ChangeSet.Change( - valueFor(this, this?.isSelfMuted), - other.isSelfMuted, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::isSelfStreaming] = - ChangeSet.Change( - valueFor(this, this?.isSelfStreaming), - other.isSelfStreaming, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::isSelfVideo] = - ChangeSet.Change( - valueFor(this, this?.isSelfVideo), - other.isSelfVideo, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::isSuppressed] = - ChangeSet.Change( - valueFor(this, this?.isSuppressed), - other.isSuppressed, - oldState, - ChangeSet.State.Present - ) - - changeSet[VoiceState::requestToSpeakTimestamp] = - ChangeSet.Change( - valueFor(this, this?.requestToSpeakTimestamp), - other.requestToSpeakTimestamp, - oldState, - ChangeSet.State.Present - ) - - return changeSet + val changeSet = ChangeSet(VoiceState::class) + + val oldState = if (this == null) { + ChangeSet.State.Missing + } else { + ChangeSet.State.Present + } + + changeSet[VoiceState::guildId] = + ChangeSet.Change( + valueFor(this, this?.guildId), + other.guildId, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::channelId] = + ChangeSet.Change( + valueFor(this, this?.channelId), + other.channelId, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::userId] = + ChangeSet.Change( + valueFor(this, this?.userId), + other.userId, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::sessionId] = + ChangeSet.Change( + valueFor(this, this?.sessionId), + other.sessionId, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::isDeafened] = + ChangeSet.Change( + valueFor(this, this?.isDeafened), + other.isDeafened, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::isMuted] = + ChangeSet.Change( + valueFor(this, this?.isMuted), + other.isMuted, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::isSelfDeafened] = + ChangeSet.Change( + valueFor(this, this?.isSelfDeafened), + other.isSelfDeafened, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::isSelfMuted] = + ChangeSet.Change( + valueFor(this, this?.isSelfMuted), + other.isSelfMuted, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::isSelfStreaming] = + ChangeSet.Change( + valueFor(this, this?.isSelfStreaming), + other.isSelfStreaming, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::isSelfVideo] = + ChangeSet.Change( + valueFor(this, this?.isSelfVideo), + other.isSelfVideo, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::isSuppressed] = + ChangeSet.Change( + valueFor(this, this?.isSuppressed), + other.isSuppressed, + oldState, + ChangeSet.State.Present + ) + + changeSet[VoiceState::requestToSpeakTimestamp] = + ChangeSet.Change( + valueFor(this, this?.requestToSpeakTimestamp), + other.requestToSpeakTimestamp, + oldState, + ChangeSet.State.Present + ) + + return changeSet } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/MemberDelta.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/MemberDelta.kt index 867a78fc9b..a4692041ff 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/MemberDelta.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/MemberDelta.kt @@ -24,59 +24,59 @@ import kotlin.contracts.contract */ @Suppress("UndocumentedPublicProperty") public class MemberDelta( - avatar: Optional, - username: Optional, - discriminator: Optional, - flags: Optional, + avatar: Optional, + username: Optional, + discriminator: Optional, + flags: Optional, - public val nickname: Optional, - public val boosting: Optional, - public val roles: Optional, - public val owner: Optional, - public val pending: Optional + public val nickname: Optional, + public val boosting: Optional, + public val roles: Optional, + public val owner: Optional, + public val pending: Optional, ) : UserDelta(avatar, username, discriminator, flags) { - /** - * A Set representing the values that have changes. Each value is represented by a human-readable string. - */ - override val changes: Set by lazy { - super.changes.toMutableSet().apply { - if (nickname !is Optional.Missing) add("nickname") - if (boosting !is Optional.Missing) add("boosting") - if (roles !is Optional.Missing) add("roles") - if (owner !is Optional.Missing) add("owner") - if (pending !is Optional.Missing) add("pending") - } - } + /** + * A Set representing the values that have changes. Each value is represented by a human-readable string. + */ + override val changes: Set by lazy { + super.changes.toMutableSet().apply { + if (nickname !is Optional.Missing) add("nickname") + if (boosting !is Optional.Missing) add("boosting") + if (roles !is Optional.Missing) add("roles") + if (owner !is Optional.Missing) add("owner") + if (pending !is Optional.Missing) add("pending") + } + } - public companion object { - /** - * Given an old and new [Member] object, return a [MemberDelta] representing the changes between them. - * - * @param old The older [Member] object. - * @param new The newer [Member] object. - */ - public suspend fun from(old: Member?, new: Member): MemberDelta? { - contract { - returns(null) implies (old == null) - } + public companion object { + /** + * Given an old and new [Member] object, return a [MemberDelta] representing the changes between them. + * + * @param old The older [Member] object. + * @param new The newer [Member] object. + */ + public suspend fun from(old: Member?, new: Member): MemberDelta? { + contract { + returns(null) implies (old == null) + } - old ?: return null + old ?: return null - val user = UserDelta.from(old, new) ?: return null - val roleDelta = RoleDelta.from(old, new) + val user = UserDelta.from(old, new) ?: return null + val roleDelta = RoleDelta.from(old, new) - return MemberDelta( - user.avatar, - user.username, - user.discriminator, - user.flags, + return MemberDelta( + user.avatar, + user.username, + user.discriminator, + user.flags, - if (old.nickname != new.nickname) Optional(new.nickname) else Optional.Missing(), - if (old.premiumSince != new.premiumSince) Optional(new.premiumSince) else Optional.Missing(), - if (roleDelta != null) Optional(roleDelta) else Optional.Missing(), - if (old.isOwner() != new.isOwner()) Optional(new.isOwner()) else Optional.Missing(), - if (old.isPending != new.isPending) Optional(new.isPending) else Optional.Missing() - ) - } - } + if (old.nickname != new.nickname) Optional(new.nickname) else Optional.Missing(), + if (old.premiumSince != new.premiumSince) Optional(new.premiumSince) else Optional.Missing(), + if (roleDelta != null) Optional(roleDelta) else Optional.Missing(), + if (old.isOwner() != new.isOwner()) Optional(new.isOwner()) else Optional.Missing(), + if (old.isPending != new.isPending) Optional(new.isPending) else Optional.Missing() + ) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/MessageDelta.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/MessageDelta.kt index 216c5c46ab..f63ec518aa 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/MessageDelta.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/MessageDelta.kt @@ -23,85 +23,85 @@ import kotlin.contracts.contract */ @Suppress("UndocumentedPublicProperty") public open class MessageDelta( - public val attachments: Optional>, - public val content: Optional, - public val editedTimestamp: Optional, - public val embeds: Optional>, - public val isPinned: Optional, - public val mentionedChannelIds: Optional>, - public val mentionedRoleIds: Optional>, - public val mentionedUserIds: Optional>, - public val mentionsEveryone: Optional, - public val reactions: Optional>, - public val stickers: Optional> + public val attachments: Optional>, + public val content: Optional, + public val editedTimestamp: Optional, + public val embeds: Optional>, + public val isPinned: Optional, + public val mentionedChannelIds: Optional>, + public val mentionedRoleIds: Optional>, + public val mentionedUserIds: Optional>, + public val mentionsEveryone: Optional, + public val reactions: Optional>, + public val stickers: Optional>, ) { - /** - * A Set representing the values that have changes. Each value is represented by a human-readable string. - */ - public open val changes: Set by lazy { - mutableSetOf().apply { - if (attachments !is Optional.Missing) add("attachments") - if (content !is Optional.Missing) add("content") - if (editedTimestamp !is Optional.Missing) add("editedTimestamp") - if (embeds !is Optional.Missing) add("embeds") - if (isPinned !is Optional.Missing) add("isPinned") - if (mentionedChannelIds !is Optional.Missing) add("mentionedChannelIds") - if (mentionedRoleIds !is Optional.Missing) add("mentionedRoleIds") - if (mentionedUserIds !is Optional.Missing) add("mentionedUserIds") - if (mentionsEveryone !is Optional.Missing) add("mentionsEveryone") - if (reactions !is Optional.Missing) add("reactions") - if (stickers !is Optional.Missing) add("stickers") - } - } + /** + * A Set representing the values that have changes. Each value is represented by a human-readable string. + */ + public open val changes: Set by lazy { + mutableSetOf().apply { + if (attachments !is Optional.Missing) add("attachments") + if (content !is Optional.Missing) add("content") + if (editedTimestamp !is Optional.Missing) add("editedTimestamp") + if (embeds !is Optional.Missing) add("embeds") + if (isPinned !is Optional.Missing) add("isPinned") + if (mentionedChannelIds !is Optional.Missing) add("mentionedChannelIds") + if (mentionedRoleIds !is Optional.Missing) add("mentionedRoleIds") + if (mentionedUserIds !is Optional.Missing) add("mentionedUserIds") + if (mentionsEveryone !is Optional.Missing) add("mentionsEveryone") + if (reactions !is Optional.Missing) add("reactions") + if (stickers !is Optional.Missing) add("stickers") + } + } - public companion object { - /** - * Given an old and new [User] object, return a [MessageDelta] representing the changes between them. - * - * @param old The older [User] object. - * @param new The newer [User] object. - */ - public fun from(old: Message?, new: Message): MessageDelta? { - contract { - returns(null) implies (old == null) - } + public companion object { + /** + * Given an old and new [User] object, return a [MessageDelta] representing the changes between them. + * + * @param old The older [User] object. + * @param new The newer [User] object. + */ + public fun from(old: Message?, new: Message): MessageDelta? { + contract { + returns(null) implies (old == null) + } - old ?: return null + old ?: return null - return MessageDelta( - if (old.attachments != new.attachments) Optional(new.attachments) else Optional.Missing(), - if (old.content != new.content) Optional(new.content) else Optional.Missing(), - if (old.editedTimestamp != new.editedTimestamp) Optional(new.editedTimestamp) else Optional.Missing(), - if (old.embeds != new.embeds) Optional(new.embeds) else Optional.Missing(), - if (old.isPinned != new.isPinned) Optional(new.isPinned) else Optional.Missing(), + return MessageDelta( + if (old.attachments != new.attachments) Optional(new.attachments) else Optional.Missing(), + if (old.content != new.content) Optional(new.content) else Optional.Missing(), + if (old.editedTimestamp != new.editedTimestamp) Optional(new.editedTimestamp) else Optional.Missing(), + if (old.embeds != new.embeds) Optional(new.embeds) else Optional.Missing(), + if (old.isPinned != new.isPinned) Optional(new.isPinned) else Optional.Missing(), - if (old.mentionedChannelIds != new.mentionedChannelIds) { - Optional(new.mentionedChannelIds) - } else { - Optional.Missing() - }, + if (old.mentionedChannelIds != new.mentionedChannelIds) { + Optional(new.mentionedChannelIds) + } else { + Optional.Missing() + }, - if (old.mentionedRoleIds != new.mentionedRoleIds) { - Optional(new.mentionedRoleIds) - } else { - Optional.Missing() - }, + if (old.mentionedRoleIds != new.mentionedRoleIds) { + Optional(new.mentionedRoleIds) + } else { + Optional.Missing() + }, - if (old.mentionedUserIds != new.mentionedUserIds) { - Optional(new.mentionedUserIds) - } else { - Optional.Missing() - }, + if (old.mentionedUserIds != new.mentionedUserIds) { + Optional(new.mentionedUserIds) + } else { + Optional.Missing() + }, - if (old.mentionsEveryone != new.mentionsEveryone) { - Optional(new.mentionsEveryone) - } else { - Optional.Missing() - }, + if (old.mentionsEveryone != new.mentionsEveryone) { + Optional(new.mentionsEveryone) + } else { + Optional.Missing() + }, - if (old.reactions != new.reactions) Optional(new.reactions) else Optional.Missing(), - if (old.stickers != new.stickers) Optional(new.stickers) else Optional.Missing(), - ) - } - } + if (old.reactions != new.reactions) Optional(new.reactions) else Optional.Missing(), + if (old.stickers != new.stickers) Optional(new.stickers) else Optional.Missing(), + ) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/RoleDelta.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/RoleDelta.kt index b60cb1e298..48256b3146 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/RoleDelta.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/RoleDelta.kt @@ -18,34 +18,34 @@ import kotlin.contracts.contract */ @Suppress("UndocumentedPublicProperty") public data class RoleDelta( - private val old: Set, - private val new: Set + private val old: Set, + private val new: Set, ) { - /** A set representing any roles that were added in [new], if any. **/ - public val added: Set = new - old - - /** A set representing any roles that removed in [new], if any. **/ - public val removed: Set = old - new - - public companion object { - /** - * Given an old and new Kord [Member] object, return a [RoleDelta] representing the changes between them. - * - * This function will return `null` if there were no changes, or if [old] is `null`. - * - * @param old The older [Member] object. - * @param new The newer [Member] object. - */ - public suspend fun from(old: Member?, new: Member): RoleDelta? { - contract { - returns(null) implies (old == null) - } - - old ?: return null - - if (old.roleIds == new.roleIds) return null - - return RoleDelta(old.roles.toSet(), new.roles.toSet()) - } - } + /** A set representing any roles that were added in [new], if any. **/ + public val added: Set = new - old + + /** A set representing any roles that removed in [new], if any. **/ + public val removed: Set = old - new + + public companion object { + /** + * Given an old and new Kord [Member] object, return a [RoleDelta] representing the changes between them. + * + * This function will return `null` if there were no changes, or if [old] is `null`. + * + * @param old The older [Member] object. + * @param new The newer [Member] object. + */ + public suspend fun from(old: Member?, new: Member): RoleDelta? { + contract { + returns(null) implies (old == null) + } + + old ?: return null + + if (old.roleIds == new.roleIds) return null + + return RoleDelta(old.roles.toSet(), new.roles.toSet()) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/UserDelta.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/UserDelta.kt index a27d566e51..527e4d709c 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/UserDelta.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/deltas/UserDelta.kt @@ -23,43 +23,43 @@ import kotlin.contracts.contract */ @Suppress("UndocumentedPublicProperty") public open class UserDelta( - public val avatar: Optional, - public val username: Optional, - public val discriminator: Optional, - public val flags: Optional + public val avatar: Optional, + public val username: Optional, + public val discriminator: Optional, + public val flags: Optional, ) { - /** - * A Set representing the values that have changes. Each value is represented by a human-readable string. - */ - public open val changes: Set by lazy { - mutableSetOf().apply { - if (avatar !is Optional.Missing) add("avatar") - if (username !is Optional.Missing) add("username") - if (discriminator !is Optional.Missing) add("discriminator") - if (flags !is Optional.Missing) add("flags") - } - } + /** + * A Set representing the values that have changes. Each value is represented by a human-readable string. + */ + public open val changes: Set by lazy { + mutableSetOf().apply { + if (avatar !is Optional.Missing) add("avatar") + if (username !is Optional.Missing) add("username") + if (discriminator !is Optional.Missing) add("discriminator") + if (flags !is Optional.Missing) add("flags") + } + } - public companion object { - /** - * Given an old and new [User] object, return a [UserDelta] representing the changes between them. - * - * @param old The older [User] object. - * @param new The newer [User] object. - */ - public fun from(old: User?, new: User): UserDelta? { - contract { - returns(null) implies (old == null) - } + public companion object { + /** + * Given an old and new [User] object, return a [UserDelta] representing the changes between them. + * + * @param old The older [User] object. + * @param new The newer [User] object. + */ + public fun from(old: User?, new: User): UserDelta? { + contract { + returns(null) implies (old == null) + } - old ?: return null + old ?: return null - return UserDelta( - if (old.avatarHash != new.avatarHash) Optional(new.avatar) else Optional.Missing(), - if (old.username != new.username) Optional(new.username) else Optional.Missing(), - if (old.discriminator != new.discriminator) Optional(new.discriminator) else Optional.Missing(), - if (old.publicFlags != new.publicFlags) Optional(new.publicFlags) else Optional.Missing() - ) - } - } + return UserDelta( + if (old.avatarHash != new.avatarHash) Optional(new.avatar) else Optional.Missing(), + if (old.username != new.username) Optional(new.username) else Optional.Missing(), + if (old.discriminator != new.discriminator) Optional(new.discriminator) else Optional.Missing(), + if (old.publicFlags != new.publicFlags) Optional(new.publicFlags) else Optional.Missing() + ) + } + } } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/scheduling/Scheduler.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/scheduling/Scheduler.kt index d9e0180561..09361ec0d4 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/scheduling/Scheduler.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/scheduling/Scheduler.kt @@ -27,82 +27,82 @@ private val logger = KotlinLogging.logger {} * Schedulers are [CoroutineScope]s and thus can be cancelled to cancel all nested jobs, if required.. */ public class Scheduler : CoroutineScope { - internal val tasks: MutableList = mutableListOf() - - override val coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob() - - /** Convenience function to schedule a [Task] using [seconds] instead of a [Duration]. **/ - public suspend fun schedule( - seconds: Long, - startNow: Boolean = true, - name: String? = null, - pollingSeconds: Long = 1, - repeat: Boolean = false, - callback: suspend () -> Unit - ): Task = schedule( - delay = seconds.seconds, - startNow = startNow, - name = name, - pollingSeconds = pollingSeconds, - repeat = repeat, - callback = callback, - ) - - /** - * Schedule a [Task] using the given [delay] and [callback]. A name will be generated if not provided. - * - * @param delay [Duration] object representing the time to wait for. - * @param startNow Whether to start the task now - `false` if you want to start it yourself. - * @param name Optional task name, used in logging. - * @param pollingSeconds How often to check whether enough time has passed - `1` by default. - * @param repeat Whether to repeat the task indefinitely - `false` by default. - * @param callback Callback to run when the task has waited for long enough. - */ - public suspend fun schedule( - delay: Duration, - startNow: Boolean = true, - name: String? = null, - pollingSeconds: Long = 1, - repeat: Boolean = false, - callback: suspend () -> Unit - ): Task { - val taskName = name ?: UUID.randomUUID().toString() - - val task = Task( - callback = callback, - coroutineScope = this, - pollingSeconds = pollingSeconds, - duration = delay, - name = taskName, - repeat = repeat, - parent = this - ) - - tasks.add(task) - - if (startNow) { - task.start() - } - - return task - } - - /** Make all child tasks complete immediately. **/ - public suspend fun callAllNow(): Unit = tasks.forEach { it.callNow() } - - /** Shut down this scheduler, cancelling all tasks. **/ - public fun shutdown() { - tasks.toList().forEach { it.cancel() } // So we don't modify while we iterate - - try { - this.cancel() - } catch (e: IllegalStateException) { - logger.debug(e) { "Scheduler cancelled with no jobs." } - } - - tasks.clear() - } - - internal fun removeTask(task: Task) = + internal val tasks: MutableList = mutableListOf() + + override val coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob() + + /** Convenience function to schedule a [Task] using [seconds] instead of a [Duration]. **/ + public suspend fun schedule( + seconds: Long, + startNow: Boolean = true, + name: String? = null, + pollingSeconds: Long = 1, + repeat: Boolean = false, + callback: suspend () -> Unit, + ): Task = schedule( + delay = seconds.seconds, + startNow = startNow, + name = name, + pollingSeconds = pollingSeconds, + repeat = repeat, + callback = callback, + ) + + /** + * Schedule a [Task] using the given [delay] and [callback]. A name will be generated if not provided. + * + * @param delay [Duration] object representing the time to wait for. + * @param startNow Whether to start the task now - `false` if you want to start it yourself. + * @param name Optional task name, used in logging. + * @param pollingSeconds How often to check whether enough time has passed - `1` by default. + * @param repeat Whether to repeat the task indefinitely - `false` by default. + * @param callback Callback to run when the task has waited for long enough. + */ + public suspend fun schedule( + delay: Duration, + startNow: Boolean = true, + name: String? = null, + pollingSeconds: Long = 1, + repeat: Boolean = false, + callback: suspend () -> Unit, + ): Task { + val taskName = name ?: UUID.randomUUID().toString() + + val task = Task( + callback = callback, + coroutineScope = this, + pollingSeconds = pollingSeconds, + duration = delay, + name = taskName, + repeat = repeat, + parent = this + ) + + tasks.add(task) + + if (startNow) { + task.start() + } + + return task + } + + /** Make all child tasks complete immediately. **/ + public suspend fun callAllNow(): Unit = tasks.forEach { it.callNow() } + + /** Shut down this scheduler, cancelling all tasks. **/ + public fun shutdown() { + tasks.toList().forEach { it.cancel() } // So we don't modify while we iterate + + try { + this.cancel() + } catch (e: IllegalStateException) { + logger.debug(e) { "Scheduler cancelled with no jobs." } + } + + tasks.clear() + } + + internal fun removeTask(task: Task) = tasks.remove(task) } diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/scheduling/Task.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/scheduling/Task.kt index f83b6d4405..6f377e9ca5 100644 --- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/scheduling/Task.kt +++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/utils/scheduling/Task.kt @@ -37,47 +37,47 @@ import kotlin.time.TimeSource */ @OptIn(ExperimentalTime::class) public open class Task( - public open var duration: Duration, - public open val callback: suspend () -> Unit, - public open var pollingSeconds: Long = 1, - public open val coroutineScope: CoroutineScope = com.kotlindiscord.kord.extensions.utils.getKoin().get(), - public open val parent: Scheduler? = null, - - public val name: String = "Unnamed", - public val repeat: Boolean = false + public open var duration: Duration, + public open val callback: suspend () -> Unit, + public open var pollingSeconds: Long = 1, + public open val coroutineScope: CoroutineScope = com.kotlindiscord.kord.extensions.utils.getKoin().get(), + public open val parent: Scheduler? = null, + + public val name: String = "Unnamed", + public val repeat: Boolean = false, ) : KordExKoinComponent { - /** Cache map for storing data for this task, if needed. **/ - public val cache: MutableStringKeyedMap = mutableMapOf() + /** Cache map for storing data for this task, if needed. **/ + public val cache: MutableStringKeyedMap = mutableMapOf() - protected val logger: KLogger = KotlinLogging.logger("Task: $name") - protected var job: Job? = null - protected lateinit var started: TimeMark - protected val sentry: SentryAdapter by inject() + protected val logger: KLogger = KotlinLogging.logger("Task: $name") + protected var job: Job? = null + protected lateinit var started: TimeMark + protected val sentry: SentryAdapter by inject() - /** - * Number of times this task has been executed. - * - * If a ULong is too small for this... wyd? - */ - public var executions: ULong = 0UL + /** + * Number of times this task has been executed. + * + * If a ULong is too small for this... wyd? + */ + public var executions: ULong = 0UL - /** Whether this task is currently running - that is, waiting until it's time to execute the [callback]. **/ - public val running: Boolean get() = job != null + /** Whether this task is currently running - that is, waiting until it's time to execute the [callback]. **/ + public val running: Boolean get() = job != null - /** Calculate whether it's time to start this task, returning `true` if so. **/ + /** Calculate whether it's time to start this task, returning `true` if so. **/ public fun shouldStart(): Boolean = started.elapsedNow() >= duration - /** Mark the start time and begin waiting until the execution time has been reached. **/ - public suspend fun start() { - val sentryContext = SentryContext() + /** Mark the start time and begin waiting until the execution time has been reached. **/ + public suspend fun start() { + val sentryContext = SentryContext() - started = TimeSource.Monotonic.markNow() + started = TimeSource.Monotonic.markNow() - if (executions == 0UL) { - sentryContext.breadcrumb(BreadcrumbType.Info) { - val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) + if (executions == 0UL) { + sentryContext.breadcrumb(BreadcrumbType.Info) { + val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) - message = "Starting task: waiting for configured delay to pass" + message = "Starting task: waiting for configured delay to pass" data["task.delay"] = duration.toIsoString() data["task.name"] = name @@ -85,105 +85,105 @@ public open class Task( data["task.repeating"] = repeat data["time.now"] = now.toString() - } - } + } + } - job = coroutineScope.launch { - while (!shouldStart()) { - @Suppress("MagicNumber") // We're just turning it into seconds from millis - delay(pollingSeconds * 1000) - } + job = coroutineScope.launch { + while (!shouldStart()) { + @Suppress("MagicNumber") // We're just turning it into seconds from millis + delay(pollingSeconds * 1000) + } - if (executions == 0UL) { + if (executions == 0UL) { sentryContext.context("task", name) - sentryContext.breadcrumb(BreadcrumbType.Info) { - val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) + sentryContext.breadcrumb(BreadcrumbType.Info) { + val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) - message = "Delay has passed, executing task (for the first time)" + message = "Delay has passed, executing task (for the first time)" data["time.now"] = now.toString() - } - } - - @Suppress("TooGenericExceptionCaught") - try { - callback() - } catch (t: Throwable) { - if (t is CancellationException && t.cause == null) { - logger.trace { "Task cancelled." } - } else { - logger.error(t) { "Error running scheduled callback." } - - if (sentry.enabled) { - sentryContext.captureThrowable(t) { - hints["executions"] = executions.toString() - } - } - } - } finally { - executions += 1UL - } - - if (!repeat) { - removeFromParent() - - job = null - } else { - start() - } - } - } - - /** Stop waiting and immediately execute the [callback]. **/ - public suspend fun callNow() { - cancel() - - @Suppress("TooGenericExceptionCaught") - try { - callback() - } catch (t: Throwable) { - logger.error(t) { "Error running scheduled callback." } - } - } - - /** Stop waiting and don't execute. **/ - public fun cancel() { - job?.cancel() - job = null - - removeFromParent() - } - - /** Like [cancel], but blocks .. **/ - public suspend fun cancelAndJoin() { - job?.cancelAndJoin() - job = null - - removeFromParent() - } - - /** If the task is running, cancel it and restart it. **/ - public suspend fun restart() { - job?.cancel() - job = null - - start() - } - - /** Like [restart], but blocks until the cancellation has been applied. **/ - public suspend fun restartJoining() { - job?.cancelAndJoin() - job = null - - start() - } - - /** Join the running [job], if any. **/ - public suspend fun join() { - job?.join() - } - - protected fun removeFromParent(): Boolean? = + } + } + + @Suppress("TooGenericExceptionCaught") + try { + callback() + } catch (t: Throwable) { + if (t is CancellationException && t.cause == null) { + logger.trace { "Task cancelled." } + } else { + logger.error(t) { "Error running scheduled callback." } + + if (sentry.enabled) { + sentryContext.captureThrowable(t) { + hints["executions"] = executions.toString() + } + } + } + } finally { + executions += 1UL + } + + if (!repeat) { + removeFromParent() + + job = null + } else { + start() + } + } + } + + /** Stop waiting and immediately execute the [callback]. **/ + public suspend fun callNow() { + cancel() + + @Suppress("TooGenericExceptionCaught") + try { + callback() + } catch (t: Throwable) { + logger.error(t) { "Error running scheduled callback." } + } + } + + /** Stop waiting and don't execute. **/ + public fun cancel() { + job?.cancel() + job = null + + removeFromParent() + } + + /** Like [cancel], but blocks .. **/ + public suspend fun cancelAndJoin() { + job?.cancelAndJoin() + job = null + + removeFromParent() + } + + /** If the task is running, cancel it and restart it. **/ + public suspend fun restart() { + job?.cancel() + job = null + + start() + } + + /** Like [restart], but blocks until the cancellation has been applied. **/ + public suspend fun restartJoining() { + job?.cancelAndJoin() + job = null + + start() + } + + /** Join the running [job], if any. **/ + public suspend fun join() { + job?.join() + } + + protected fun removeFromParent(): Boolean? = parent?.removeTask(this@Task) } diff --git a/kord-extensions/src/main/resources/translations/kordex/strings.properties b/kord-extensions/src/main/resources/translations/kordex/strings.properties index d26f716bbe..9ff427aa31 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Error in argument** `{0}`**:** {1} argumentParser.error.requiresOneValue=Argument `{0}` requires exactly 1 value, but {1} were provided. argumentParser.error.invalidValue=Invalid value for argument `{0}` (which accepts: {1}) @@ -44,7 +43,6 @@ checks.hasPermission.failed=Must have permission: **{0}** checks.notHasPermission.failed=Must not have permission: **{0}** checks.hasPermissions.failed=Must have permissions: **{0}** checks.notHasPermissions.failed=Must not have permissions: **{0}** - checks.channelIsNsfw.failed=Must be in an NSFW channel checks.notChannelIsNsfw.failed=Must not be in an NSFW channel checks.guildNsfwLevelEqual.failed=Must be in a server with NSFW level: **{0}** @@ -53,7 +51,6 @@ checks.guildNsfwLevelHigher.failed=Must be in a server with an NSFW level higher checks.guildNsfwLevelHigherOrEqual.failed=Must be in a server with an NSFW level of **{0}**, or higher checks.guildNsfwLevelLower.failed=Must be in a server with an NSFW level lower than: **{0}** checks.guildNsfwLevelLowerOrEqual.failed=Must be in a server with an NSFW level of **{0}**, or lower - checks.isBot.failed=Must be a bot checks.isBotAdmin.failed=Must be one of this bot's admins checks.isBotOwner.failed=Must be this bot's owner diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_ar.properties b/kord-extensions/src/main/resources/translations/kordex/strings_ar.properties index cc5c0b50d9..3b5a8322f9 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_ar.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_ar.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - channelType.guildDirectory= channelType.guildNews= channelType.guildStageVoice= diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_de_DE.properties b/kord-extensions/src/main/resources/translations/kordex/strings_de_DE.properties index 162011971c..c84564b22c 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_de_DE.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_de_DE.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Fehler in Argument** `{0}`**:** {1} argumentParser.error.requiresOneValue=Das Argument `{0}` benötigt genau einen Wert, aber {1} wurden angegeben. argumentParser.error.invalidValue=Ungültiger Wert für das Argument `{0}` (akzeptiert: {1}) diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_en_GB.properties b/kord-extensions/src/main/resources/translations/kordex/strings_en_GB.properties index cbd01cafba..ded60d6378 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_en_GB.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_en_GB.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Error in argument** `{0}`**:** {1} argumentParser.error.requiresOneValue=Argument `{0}` requires exactly 1 value, but {1} were provided. argumentParser.error.invalidValue=Invalid value for argument `{0}` (which accepts: {1}) diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_es.properties b/kord-extensions/src/main/resources/translations/kordex/strings_es.properties index 9ab103758b..257d29ecd1 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_es.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_es.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.unknownConverterType=Tipo de convertidor desconocido proveído: `{0}` channelType.guildVoice=Voz channelType.groupDm=Grupo de MD diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_fi_FI.properties b/kord-extensions/src/main/resources/translations/kordex/strings_fi_FI.properties index df4b443a62..cafa55bc52 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_fi_FI.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_fi_FI.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Virhe argumentissa** `{0}`**:** {1} argumentParser.error.requiresOneValue=Argumentti `{0}` vaatii täsmälleen yhden arvon, mutta annettiin {1}. argumentParser.error.invalidValue=Kelvoton arvo argumentille `{0}` (mikä hyväksyy tyypin {1}) diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_fr_FR.properties b/kord-extensions/src/main/resources/translations/kordex/strings_fr_FR.properties index 873d97b61f..b903dab3af 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_fr_FR.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_fr_FR.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Erreur dans l’argument** `{0}`**:** {1} argumentParser.error.requiresOneValue=L’argument `{0}` requiert exactement 1 valeur, mais {1} ont été fournis. argumentParser.error.invalidValue=Valeur invalide pour l’argument `{0}` (qui accepte {1}) diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_it.properties b/kord-extensions/src/main/resources/translations/kordex/strings_it.properties index c595b480dd..d853a885a1 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_it.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_it.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - channelType.guildDirectory= channelType.privateThread= channelType.unknown= diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_ja.properties b/kord-extensions/src/main/resources/translations/kordex/strings_ja.properties index ad5a2b7482..5ba6450cdf 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_ja.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_ja.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - channelType.guildDirectory= channelType.publicGuildThread= channelType.privateThread= diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_ko.properties b/kord-extensions/src/main/resources/translations/kordex/strings_ko.properties index c860d5afe9..38ecdb7b94 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_ko.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_ko.properties @@ -1,5 +1,3 @@ - - argumentParser.error.notAllValid=인자 `{0}`은 {1} {1, plural, =1 {개의 값} other {개의 값들}}을 입력받았지만, {3}에 {2, plural, =0 {전부 적절하지 않았습니다} other {오직 {2}개만 적절한 값이었습니다 }}. argumentParser.error.invalidValue=인자 `{0}`에 지원되지 않는 값 ({1} 입력 가능) argumentParser.error.unknownConverterType=알 수 없는 변환기 형식: `{0}` diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_pl_PL.properties b/kord-extensions/src/main/resources/translations/kordex/strings_pl_PL.properties index aba598303e..6839c053ce 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_pl_PL.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_pl_PL.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Błąd w argumencie** `{0}`**:** {1} argumentParser.error.requiresOneValue=Argument `{0}` wymaga dokładnie 1 wartość, ale {1} została podana. argumentParser.error.invalidValue=Nieprawidłowa wartość dla argumentu `{0}` (który akceptuje: {1}) diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_pt_PT.properties b/kord-extensions/src/main/resources/translations/kordex/strings_pt_PT.properties index d14fdd35d8..99c2b043d5 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_pt_PT.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_pt_PT.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Erro no argumento** `{0}`**:** {1} argumentParser.error.requiresOneValue=O argumento `{0}` necessita de exatamente 1 valor, mas {1} foram fornecidos. argumentParser.error.invalidValue=Valor do argumento `{0}` é inválido (aceita: {1}) diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_ru_RU.properties b/kord-extensions/src/main/resources/translations/kordex/strings_ru_RU.properties index 73c5aa9776..da46934746 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_ru_RU.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_ru_RU.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Ошибка в аргументе** `{0}` **:** {1} argumentParser.error.requiresOneValue=Аргумент `{0}` требует ровно одного значения, дано {1}. argumentParser.error.invalidValue=Неверное значение для аргумента `{0}` (ожидалось: {1}) diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_tok.properties b/kord-extensions/src/main/resources/translations/kordex/strings_tok.properties index f700cc1954..c035c3300e 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_tok.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_tok.properties @@ -1,5 +1,3 @@ - - checks.isInThread.failed=o lon tomo lili checks.anyGuild.failed=o lon ma checks.notInGuild.failed=o lon ala ma **{0}** diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_tr.properties b/kord-extensions/src/main/resources/translations/kordex/strings_tr.properties index bb8fc29229..90e1f52cdd 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_tr.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_tr.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**Argümanda hata**`{0}`**:** {1} argumentParser.error.invalidValue=Argüman`{0}` için geçersiz değer (kabul edilen:{1}) argumentParser.error.unknownConverterType=Bilinmeyen dönüştürücü tipi sağlandı:`{0}` diff --git a/kord-extensions/src/main/resources/translations/kordex/strings_zh_CN.properties b/kord-extensions/src/main/resources/translations/kordex/strings_zh_CN.properties index 4709760492..70fb663ad7 100644 --- a/kord-extensions/src/main/resources/translations/kordex/strings_zh_CN.properties +++ b/kord-extensions/src/main/resources/translations/kordex/strings_zh_CN.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - argumentParser.error.errorInArgument=**参数**`{0}`**中出现错误:**{1} argumentParser.error.requiresOneValue=参数`{0}`只需要一个值,却收到了{1}个。 argumentParser.error.invalidValue=给参数`{0}`提供的值无效(可接受{1}) diff --git a/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TimestampConverterTest.kt b/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TimestampConverterTest.kt index 7c1e11832d..255bbe09c2 100644 --- a/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TimestampConverterTest.kt +++ b/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/commands/converters/impl/TimestampConverterTest.kt @@ -13,33 +13,33 @@ import org.junit.jupiter.api.Test internal class TimestampConverterTest { - @Test - fun `timestamp without format`() { - val timestamp = "" // 1st second of 2015 - val parsed = TimestampConverter.parseFromString(timestamp)!! - assertEquals(Instant.fromEpochSeconds(1_420_070_400), parsed.instant) - assertEquals(TimestampType.Default, parsed.format) - } + @Test + fun `timestamp without format`() { + val timestamp = "" // 1st second of 2015 + val parsed = TimestampConverter.parseFromString(timestamp)!! + assertEquals(Instant.fromEpochSeconds(1_420_070_400), parsed.instant) + assertEquals(TimestampType.Default, parsed.format) + } - @Test - fun `timestamp with format`() { - val timestamp = "" - val parsed = TimestampConverter.parseFromString(timestamp)!! - assertEquals(Instant.fromEpochSeconds(1_420_070_400), parsed.instant) - assertEquals(TimestampType.RelativeTime, parsed.format) - } + @Test + fun `timestamp with format`() { + val timestamp = "" + val parsed = TimestampConverter.parseFromString(timestamp)!! + assertEquals(Instant.fromEpochSeconds(1_420_070_400), parsed.instant) + assertEquals(TimestampType.RelativeTime, parsed.format) + } - @Test - fun `empty timestamp`() { - val timestamp = "" - val parsed = TimestampConverter.parseFromString(timestamp) - assertNull(parsed) - } + @Test + fun `empty timestamp`() { + val timestamp = "" + val parsed = TimestampConverter.parseFromString(timestamp) + assertNull(parsed) + } - @Test - fun `timestamp with empty format`() { - val timestamp = "" - val parsed = TimestampConverter.parseFromString(timestamp) - assertNull(parsed) - } + @Test + fun `timestamp with empty format`() { + val timestamp = "" + val parsed = TimestampConverter.parseFromString(timestamp) + assertNull(parsed) + } } diff --git a/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/test/SchedulerTest.kt b/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/test/SchedulerTest.kt index 396ba56cf9..2b2c812ef5 100644 --- a/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/test/SchedulerTest.kt +++ b/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/test/SchedulerTest.kt @@ -22,95 +22,95 @@ import kotlin.time.Duration.Companion.seconds @TestInstance(TestInstance.Lifecycle.PER_CLASS) @ExtendWith(KoinExtension::class) class SchedulerTest { - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Schedulers can be cancelled`() = runBlocking { - val scheduler = Scheduler() - var count = 0 + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Schedulers can be cancelled`() = runBlocking { + val scheduler = Scheduler() + var count = 0 - scheduler.schedule(3) { count += 1 } - scheduler.shutdown() + scheduler.schedule(3) { count += 1 } + scheduler.shutdown() - delay(3000) + delay(3000) - assertEquals(count, 0) { "Task executed when it should have been cancelled" } - } + assertEquals(count, 0) { "Task executed when it should have been cancelled" } + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Tasks can be cancelled`() = runBlocking { - val scheduler = Scheduler() - var count = 0 + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Tasks can be cancelled`() = runBlocking { + val scheduler = Scheduler() + var count = 0 - val task = scheduler.schedule(3) { - count += 1 - } + val task = scheduler.schedule(3) { + count += 1 + } - task.cancel() + task.cancel() - delay(3000) + delay(3000) - assertEquals(count, 0) { "Task executed when it should have been cancelled" } - } + assertEquals(count, 0) { "Task executed when it should have been cancelled" } + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Tasks remove themselves from the task list`() = runBlocking { - val scheduler = Scheduler() - var count = 0 + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Tasks remove themselves from the task list`() = runBlocking { + val scheduler = Scheduler() + var count = 0 - val tasks = mutableListOf() + val tasks = mutableListOf() - tasks += scheduler.schedule(10) { count += 1 } - tasks += scheduler.schedule(10) { count += 1 } - tasks += scheduler.schedule(10) { count += 1 } + tasks += scheduler.schedule(10) { count += 1 } + tasks += scheduler.schedule(10) { count += 1 } + tasks += scheduler.schedule(10) { count += 1 } - assertEquals( - scheduler.tasks.size, - tasks.size - ) { "Scheduler should have ${tasks.size} tasks, but it has ${scheduler.tasks.size}" } + assertEquals( + scheduler.tasks.size, + tasks.size + ) { "Scheduler should have ${tasks.size} tasks, but it has ${scheduler.tasks.size}" } - tasks.forEach { it.callNow() } + tasks.forEach { it.callNow() } - delay(1000) // Some systems are a bit weird about job timing + delay(1000) // Some systems are a bit weird about job timing - assertEquals( - scheduler.tasks.size, - 0 - ) { "Scheduler should have 0 tasks, but it has ${scheduler.tasks.size}" } - } + assertEquals( + scheduler.tasks.size, + 0 + ) { "Scheduler should have 0 tasks, but it has ${scheduler.tasks.size}" } + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Tasks run exactly once`() = runBlocking { - val scheduler = Scheduler() - var count = 0 + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Tasks run exactly once`() = runBlocking { + val scheduler = Scheduler() + var count = 0 - val task = scheduler.schedule(0) { - count += 1 - } + val task = scheduler.schedule(0) { + count += 1 + } - if (task.running) { - task.join() - } + if (task.running) { + task.join() + } - assertEquals(count, 1) { "Task executed $count times instead of once" } - } + assertEquals(count, 1) { "Task executed $count times instead of once" } + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Tasks marked as repeatable run multiple times`() = runBlocking { - val scheduler = Scheduler() - var count = 0 + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Tasks marked as repeatable run multiple times`() = runBlocking { + val scheduler = Scheduler() + var count = 0 - val task = scheduler.schedule(1.seconds, repeat = true) { - count += 1 - } + val task = scheduler.schedule(1.seconds, repeat = true) { + count += 1 + } - delay(10.seconds) + delay(10.seconds) - task.cancel() + task.cancel() - assert(count > 1) { "Task did not run multiple times" } - } + assert(count > 1) { "Task did not run multiple times" } + } } diff --git a/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/utils/MapTest.kt b/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/utils/MapTest.kt index b18f289041..fc171770f9 100644 --- a/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/utils/MapTest.kt +++ b/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/utils/MapTest.kt @@ -17,32 +17,32 @@ import kotlin.test.assertNull * Tests for [Map] extension functions. */ class MapTest { - /** - * Check that the typed map getters return the values expected of them. - */ - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `check typed getters`() { - val map: MutableStringKeyedMap = mutableMapOf( - "string" to "value", - "number" to 1 - ) - - assertEquals(1, map.getOf("number")) - assertEquals("value", map.getOf("string")) - - assertNull(map.getOfOrNull("missing")) - assertEquals(2, map.getOrDefault("missing", 2)) - - assertThrows { map.getOf("number") } - assertThrows { map.getOf("missing") } - - assertEquals("two", map.getOfOrDefault("two", "two", true)) - assertEquals("two", map.getOf("two")) - assertThrows { map.getOf("two") } - - assertEquals(2, map.getOfOrDefault("two", 2, true)) - assertEquals(2, map.getOf("two")) - assertThrows { map.getOf("two") } - } + /** + * Check that the typed map getters return the values expected of them. + */ + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `check typed getters`() { + val map: MutableStringKeyedMap = mutableMapOf( + "string" to "value", + "number" to 1 + ) + + assertEquals(1, map.getOf("number")) + assertEquals("value", map.getOf("string")) + + assertNull(map.getOfOrNull("missing")) + assertEquals(2, map.getOrDefault("missing", 2)) + + assertThrows { map.getOf("number") } + assertThrows { map.getOf("missing") } + + assertEquals("two", map.getOfOrDefault("two", "two", true)) + assertEquals("two", map.getOf("two")) + assertThrows { map.getOf("two") } + + assertEquals(2, map.getOfOrDefault("two", 2, true)) + assertEquals(2, map.getOf("two")) + assertThrows { map.getOf("two") } + } } diff --git a/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/utils/StringTest.kt b/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/utils/StringTest.kt index 7f03dd38a7..400bf5c389 100644 --- a/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/utils/StringTest.kt +++ b/kord-extensions/src/test/kotlin/com/kotlindiscord/kord/extensions/utils/StringTest.kt @@ -19,53 +19,53 @@ import kotlin.test.assertNotNull */ class StringTest { - /** - * Check that `.toReaction()` for a unicode emoji transforms the string correctly. - */ - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `unicode to reaction`() { - val unicode = "❤" - val reaction = unicode.toReaction() - assertEquals(unicode, reaction.name) - } + /** + * Check that `.toReaction()` for a unicode emoji transforms the string correctly. + */ + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `unicode to reaction`() { + val unicode = "❤" + val reaction = unicode.toReaction() + assertEquals(unicode, reaction.name) + } - /** - * Check that `.toReaction()` for custom emojis transforms the string correctly. - */ - @Suppress("UnderscoresInNumericLiterals", "MagicNumber") - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `custom to reaction`() { - val staticString = "<:cozy:859058952393457694>" - val staticReaction = staticString.toReaction() as? ReactionEmoji.Custom + /** + * Check that `.toReaction()` for custom emojis transforms the string correctly. + */ + @Suppress("UnderscoresInNumericLiterals", "MagicNumber") + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `custom to reaction`() { + val staticString = "<:cozy:859058952393457694>" + val staticReaction = staticString.toReaction() as? ReactionEmoji.Custom - assertNotNull(staticReaction, "Returned ReactionEmoji object must be a custom emoji") + assertNotNull(staticReaction, "Returned ReactionEmoji object must be a custom emoji") - assertEquals(staticReaction.name, "cozy") - assertEquals(staticReaction.id, Snowflake(859058952393457694)) - assertEquals(staticReaction.isAnimated, false) + assertEquals(staticReaction.name, "cozy") + assertEquals(staticReaction.id, Snowflake(859058952393457694)) + assertEquals(staticReaction.isAnimated, false) - val animatedString = "" - val animatedReaction = animatedString.toReaction() as? ReactionEmoji.Custom + val animatedString = "" + val animatedReaction = animatedString.toReaction() as? ReactionEmoji.Custom - assertNotNull(animatedReaction, "Returned ReactionEmoji object must be a custom emoji") + assertNotNull(animatedReaction, "Returned ReactionEmoji object must be a custom emoji") - assertEquals(animatedReaction.name, "ablobzerogravity") - assertEquals(animatedReaction.id, Snowflake(614485981021601812)) - assertEquals(animatedReaction.isAnimated, true) - } + assertEquals(animatedReaction.name, "ablobzerogravity") + assertEquals(animatedReaction.id, Snowflake(614485981021601812)) + assertEquals(animatedReaction.isAnimated, true) + } - /** - * Check that `.splitOn()` correctly splits a string into a pair with the given separator. - */ - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `splitting strings returns the correct pairs`() { - assertEquals("Kord" to "-Ext", "Kord-Ext".splitOn { it == '-' }) - assertEquals("KordExt" to "", "KordExt".splitOn { it == '-' }) - assertEquals("Disc" to "ord bot", "Discord bot".splitOn { it == 'o' }) + /** + * Check that `.splitOn()` correctly splits a string into a pair with the given separator. + */ + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `splitting strings returns the correct pairs`() { + assertEquals("Kord" to "-Ext", "Kord-Ext".splitOn { it == '-' }) + assertEquals("KordExt" to "", "KordExt".splitOn { it == '-' }) + assertEquals("Disc" to "ord bot", "Discord bot".splitOn { it == 'o' }) - assertEquals("Discord bot" to "", "Discord bot".splitOn { it == 'x' }) - } + assertEquals("Discord bot" to "", "Discord bot".splitOn { it == 'x' }) + } } diff --git a/kord-extensions/src/test/resources/junit-platform.properties b/kord-extensions/src/test/resources/junit-platform.properties index 580f511dad..6939a7fb02 100644 --- a/kord-extensions/src/test/resources/junit-platform.properties +++ b/kord-extensions/src/test/resources/junit-platform.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - junit.jupiter.execution.parallel.enabled=true diff --git a/kord-extensions/src/test/resources/logback.groovy b/kord-extensions/src/test/resources/logback.groovy index a0c69989f4..659357fe56 100644 --- a/kord-extensions/src/test/resources/logback.groovy +++ b/kord-extensions/src/test/resources/logback.groovy @@ -13,28 +13,28 @@ def environment = System.getenv("ENVIRONMENT") ?: "dev" def defaultLevel = TRACE if (environment == "spam") { - logger("dev.kord.rest.DefaultGateway", TRACE) + logger("dev.kord.rest.DefaultGateway", TRACE) } else { - // Silence warning about missing native PRNG - logger("io.ktor.util.random", ERROR) + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) } appender("CONSOLE", ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" - withJansi = true - } + withJansi = true + } - target = ConsoleTarget.SystemOut + target = ConsoleTarget.SystemOut } appender("FILE", FileAppender) { - file = "output.log" + file = "output.log" - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } } root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/kord-extensions/src/test/resources/translations/test/strings.properties b/kord-extensions/src/test/resources/translations/test/strings.properties index 3a80e83cc0..00b7245ade 100644 --- a/kord-extensions/src/test/resources/translations/test/strings.properties +++ b/kord-extensions/src/test/resources/translations/test/strings.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - banana=banana diff --git a/kord-extensions/src/test/resources/translations/test/strings_en_GB.properties b/kord-extensions/src/test/resources/translations/test/strings_en_GB.properties index 3a80e83cc0..00b7245ade 100644 --- a/kord-extensions/src/test/resources/translations/test/strings_en_GB.properties +++ b/kord-extensions/src/test/resources/translations/test/strings_en_GB.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - banana=banana diff --git a/kord-extensions/src/test/resources/translations/test/strings_en_US.properties b/kord-extensions/src/test/resources/translations/test/strings_en_US.properties index 3a80e83cc0..00b7245ade 100644 --- a/kord-extensions/src/test/resources/translations/test/strings_en_US.properties +++ b/kord-extensions/src/test/resources/translations/test/strings_en_US.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - banana=banana diff --git a/kord-extensions/src/test/resources/translations/test/strings_ja.properties b/kord-extensions/src/test/resources/translations/test/strings_ja.properties index 3fb299b679..8920fe65ca 100644 --- a/kord-extensions/src/test/resources/translations/test/strings_ja.properties +++ b/kord-extensions/src/test/resources/translations/test/strings_ja.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - banana=バナナ diff --git a/modules/java-time/build.gradle.kts b/modules/java-time/build.gradle.kts index 0474b1cf41..539d244563 100644 --- a/modules/java-time/build.gradle.kts +++ b/modules/java-time/build.gradle.kts @@ -1,35 +1,35 @@ plugins { - `kordex-module` - `published-module` - `ksp-module` + `kordex-module` + `published-module` + `ksp-module` - kotlin("plugin.serialization") + kotlin("plugin.serialization") } metadata { - name = "KordEx: Java Time" - description = "KordEx module that provides converters that support Java Time" + name = "KordEx: Java Time" + description = "KordEx module that provides converters that support Java Time" } dependencies { - implementation(libs.kotlin.stdlib) + implementation(libs.kotlin.stdlib) - implementation(project(":kord-extensions")) - implementation(project(":annotations")) + implementation(project(":kord-extensions")) + implementation(project(":annotations")) - ksp(project(":annotation-processor")) + ksp(project(":annotation-processor")) - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - testImplementation(libs.groovy) // For logback config - testImplementation(libs.jansi) - testImplementation(libs.junit) - testImplementation(libs.logback) - testImplementation(libs.logback.groovy) + testImplementation(libs.groovy) // For logback config + testImplementation(libs.jansi) + testImplementation(libs.junit) + testImplementation(libs.logback) + testImplementation(libs.logback.groovy) } dokkaModule { - moduleName.set("Kord Extensions: Java Time") + moduleName.set("Kord Extensions: Java Time") } diff --git a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/ChronoContainer.kt b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/ChronoContainer.kt index 10b6f2233c..7d3d9240e5 100644 --- a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/ChronoContainer.kt +++ b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/ChronoContainer.kt @@ -14,20 +14,20 @@ import java.time.temporal.ChronoUnit import java.time.temporal.Temporal private val SUPPORTED_UNITS = arrayOf( - ChronoUnit.SECONDS, - ChronoUnit.MINUTES, - ChronoUnit.HOURS, + ChronoUnit.SECONDS, + ChronoUnit.MINUTES, + ChronoUnit.HOURS, - ChronoUnit.DAYS, - ChronoUnit.MONTHS, - ChronoUnit.YEARS + ChronoUnit.DAYS, + ChronoUnit.MONTHS, + ChronoUnit.YEARS ) private val CONVERTED_UNITS = arrayOf( - ChronoUnit.WEEKS, - ChronoUnit.DECADES, - ChronoUnit.CENTURIES, - ChronoUnit.MILLENNIA + ChronoUnit.WEEKS, + ChronoUnit.DECADES, + ChronoUnit.CENTURIES, + ChronoUnit.MILLENNIA ) private const val DAYS_PER_WEEK = 7L @@ -50,256 +50,256 @@ private val logger = KotlinLogging.logger { } */ @Serializable(with = ChronoContainerSerializer::class) public class ChronoContainer { - internal val values: MutableMap = mutableMapOf() - - /** Check whether a given [ChronoUnit] is fully supported. **/ - public fun isSupported(unit: ChronoUnit): Boolean = unit in SUPPORTED_UNITS - - /** Check whether a given [ChronoUnit] will be converted in [plus], [minus] and [set]. **/ - public fun isConverted(unit: ChronoUnit): Boolean = unit in CONVERTED_UNITS - - /** - * Check whether this container contains a positive set of values. - * - * Will most likely be inaccurate if the container wasn't normalized first. - */ - public fun isPositive(): Boolean = values[ - values.filter { (_, value) -> value != 0L } - .keys - .sortedByDescending { it.duration.seconds } - .first() - ]!! >= 0L - - /** - * Create a new ChronoContainer using the values from this one. - */ - public fun clone(): ChronoContainer { - val new = ChronoContainer() - - new.values.putAll(values) - - return new - } - - /** - * Given a value and [ChronoUnit], add it to this container's collection of values. - * - * This does not transform the value via abs(), so negative values are supported. - */ - public fun plus(value: Long, unit: ChronoUnit) { - when (unit) { - in SUPPORTED_UNITS -> values[unit] = (values[unit] ?: 0) + value - - ChronoUnit.WEEKS -> plus(value * DAYS_PER_WEEK, ChronoUnit.DAYS) - ChronoUnit.DECADES -> plus(value * YEARS_PER_DECADE, ChronoUnit.YEARS) - ChronoUnit.CENTURIES -> plus(value * YEARS_PER_CENTURY, ChronoUnit.YEARS) - ChronoUnit.MILLENNIA -> plus(value * YEARS_PER_MILLENNIUM, ChronoUnit.YEARS) - - else -> throw InvalidTimeUnitException(unit.name) - } - } - - /** - * Given a value and [ChronoUnit], subtract it from this container's collection of values. - * - * This does not transform the value via abs(), so negative values are supported. - */ - public fun minus(value: Long, unit: ChronoUnit) { - when (unit) { - in SUPPORTED_UNITS -> values[unit] = (values[unit] ?: 0) - value - - ChronoUnit.WEEKS -> minus(value * DAYS_PER_WEEK, ChronoUnit.DAYS) - ChronoUnit.DECADES -> minus(value * YEARS_PER_DECADE, ChronoUnit.YEARS) - ChronoUnit.CENTURIES -> minus(value * YEARS_PER_CENTURY, ChronoUnit.YEARS) - ChronoUnit.MILLENNIA -> minus(value * YEARS_PER_MILLENNIUM, ChronoUnit.YEARS) - - else -> throw InvalidTimeUnitException(unit.name) - } - } - - /** - * Get the stored value for a given supported [ChronoUnit], additionally returning 0 if no value is found. - */ - public fun get(unit: ChronoUnit): Long = if (unit in SUPPORTED_UNITS) { - values[unit] ?: 0 - } else { - throw InvalidTimeUnitException(unit.name) - } - - /** - * Given a value and [ChronoUnit], replace any stored value for that unit with the given value. - */ - public fun set(value: Long, unit: ChronoUnit) { - when (unit) { - in SUPPORTED_UNITS -> values[unit] = value - - ChronoUnit.WEEKS -> set(value * DAYS_PER_WEEK, ChronoUnit.DAYS) - ChronoUnit.DECADES -> set(value * YEARS_PER_DECADE, ChronoUnit.YEARS) - ChronoUnit.CENTURIES -> set(value * YEARS_PER_CENTURY, ChronoUnit.YEARS) - ChronoUnit.MILLENNIA -> set(value * YEARS_PER_MILLENNIUM, ChronoUnit.YEARS) - - else -> throw InvalidTimeUnitException(unit.name) - } - } - - /** - * Given a [LocalDateTime] (defaulting to `now()`), normalize the stored values by applying them to the datetime - * and calculating their real-world difference. This will replace all stored values with newly-normalized values. - */ - public fun normalize(dateTime: LocalDateTime = LocalDateTime.now()) { - var newDateTime = dateTime - - values.forEach { unit, value -> - newDateTime = newDateTime.plus(value, unit) - } - - val nowTime = LocalDateTime.now() - val futureTime = nowTime.plusHours(get(ChronoUnit.HOURS) % HOURS_PER_DAY) - .plusMinutes(get(ChronoUnit.MINUTES) % MINUTES_PER_HOUR) - .plusSeconds(get(ChronoUnit.SECONDS) % SECONDS_PER_MINUTE) - - val timePeriodDelta = Period.between(nowTime.toLocalDate(), futureTime.toLocalDate()) - - val duration = Duration.between(dateTime, newDateTime) - val period = Period.between(dateTime.toLocalDate(), newDateTime.toLocalDate()) - .minusDays(timePeriodDelta.days.toLong()) - - copyFrom(duration, period) - } - - /** - * Given a [Temporal] subclass, apply the values in this container to it and return the result. - * - * This function will skip units that the [Temporal] doesn't support. - */ - public fun apply(target: T): T { - var result = target - - @Suppress("UNCHECKED_CAST") - values.forEach { unit, value -> - if (target.isSupported(unit)) { - result = result.plus(value, unit) as T - } else { - logger.trace { "Unit $unit is not supported by $target" } - } - } - - return result - } - - /** - * Given a [Duration] and [Period], replace all values in this container with values contained within the passed - * objects. - */ - public fun copyFrom(duration: Duration, period: Period) { - SUPPORTED_UNITS.forEach { unit -> - when (unit) { - ChronoUnit.SECONDS -> values[unit] = duration.toSecondsPart().toLong() - ChronoUnit.MINUTES -> values[unit] = duration.toMinutesPart().toLong() - ChronoUnit.HOURS -> values[unit] = duration.toHoursPart().toLong() - - ChronoUnit.DAYS -> values[unit] = period.days.toLong() - ChronoUnit.MONTHS -> values[unit] = period.months.toLong() - ChronoUnit.YEARS -> values[unit] = period.years.toLong() - - else -> {} - } - } - } - - /** - * Given a [Duration], replace all time-relevant values with values contained within the passed object. - */ - public fun copyFrom(duration: Duration) { - SUPPORTED_UNITS.forEach { unit -> - when (unit) { - ChronoUnit.SECONDS -> values[unit] = duration.toSecondsPart().toLong() - ChronoUnit.MINUTES -> values[unit] = duration.toMinutesPart().toLong() - ChronoUnit.HOURS -> values[unit] = duration.toHoursPart().toLong() - - else -> {} - } - } - } - - /** - * Given a [Period], replace all date-relevant values with values contained within the passed object. - */ - public fun copyFrom(period: Period) { - SUPPORTED_UNITS.forEach { unit -> - when (unit) { - ChronoUnit.DAYS -> values[unit] = period.days.toLong() - ChronoUnit.MONTHS -> values[unit] = period.months.toLong() - ChronoUnit.YEARS -> values[unit] = period.years.toLong() - - else -> {} - } - } - } - - public companion object { - /** - * Create a ChronoUnit and populate it with the values contained in the passed [Duration] and [Period] objects. - */ - public fun of(duration: Duration, period: Period): ChronoContainer { - val container = ChronoContainer() - - container.copyFrom(duration, period) - - return container - } - - /** - * Create a ChronoUnit and populate it with the values contained in the passed [Duration] object. - */ - public fun of(duration: Duration): ChronoContainer { - val container = ChronoContainer() - - container.copyFrom(duration) - - return container - } - - /** - * Create a ChronoUnit and populate it with the values contained in the passed [Period] object. - */ - public fun of(period: Period): ChronoContainer { - val container = ChronoContainer() - - container.copyFrom(period) - - return container - } - - /** - * Given two [LocalDateTime] objects, calculate the difference between them and return a new ChronoUnit, - * pre-populated with that difference. - */ - public fun between(before: LocalDateTime, after: LocalDateTime): ChronoContainer { - val duration = Duration.between(before, after) - val period = Period.between(before.toLocalDate(), after.toLocalDate()) - - return of(duration, period) - } - - /** - * Given two [LocalDate] objects, calculate the difference between them and return a new ChronoUnit, - * pre-populated with that difference. - */ - public fun between(before: LocalDate, after: LocalDate): ChronoContainer { - val period = Period.between(before, after) - - return of(period) - } - - /** - * Given two [LocalTime] objects, calculate the difference between them and return a new ChronoUnit, - * pre-populated with that difference. - */ - public fun between(before: LocalTime, after: LocalTime): ChronoContainer { - val duration = Duration.between(before, after) - - return of(duration) - } - } + internal val values: MutableMap = mutableMapOf() + + /** Check whether a given [ChronoUnit] is fully supported. **/ + public fun isSupported(unit: ChronoUnit): Boolean = unit in SUPPORTED_UNITS + + /** Check whether a given [ChronoUnit] will be converted in [plus], [minus] and [set]. **/ + public fun isConverted(unit: ChronoUnit): Boolean = unit in CONVERTED_UNITS + + /** + * Check whether this container contains a positive set of values. + * + * Will most likely be inaccurate if the container wasn't normalized first. + */ + public fun isPositive(): Boolean = values[ + values.filter { (_, value) -> value != 0L } + .keys + .sortedByDescending { it.duration.seconds } + .first() + ]!! >= 0L + + /** + * Create a new ChronoContainer using the values from this one. + */ + public fun clone(): ChronoContainer { + val new = ChronoContainer() + + new.values.putAll(values) + + return new + } + + /** + * Given a value and [ChronoUnit], add it to this container's collection of values. + * + * This does not transform the value via abs(), so negative values are supported. + */ + public fun plus(value: Long, unit: ChronoUnit) { + when (unit) { + in SUPPORTED_UNITS -> values[unit] = (values[unit] ?: 0) + value + + ChronoUnit.WEEKS -> plus(value * DAYS_PER_WEEK, ChronoUnit.DAYS) + ChronoUnit.DECADES -> plus(value * YEARS_PER_DECADE, ChronoUnit.YEARS) + ChronoUnit.CENTURIES -> plus(value * YEARS_PER_CENTURY, ChronoUnit.YEARS) + ChronoUnit.MILLENNIA -> plus(value * YEARS_PER_MILLENNIUM, ChronoUnit.YEARS) + + else -> throw InvalidTimeUnitException(unit.name) + } + } + + /** + * Given a value and [ChronoUnit], subtract it from this container's collection of values. + * + * This does not transform the value via abs(), so negative values are supported. + */ + public fun minus(value: Long, unit: ChronoUnit) { + when (unit) { + in SUPPORTED_UNITS -> values[unit] = (values[unit] ?: 0) - value + + ChronoUnit.WEEKS -> minus(value * DAYS_PER_WEEK, ChronoUnit.DAYS) + ChronoUnit.DECADES -> minus(value * YEARS_PER_DECADE, ChronoUnit.YEARS) + ChronoUnit.CENTURIES -> minus(value * YEARS_PER_CENTURY, ChronoUnit.YEARS) + ChronoUnit.MILLENNIA -> minus(value * YEARS_PER_MILLENNIUM, ChronoUnit.YEARS) + + else -> throw InvalidTimeUnitException(unit.name) + } + } + + /** + * Get the stored value for a given supported [ChronoUnit], additionally returning 0 if no value is found. + */ + public fun get(unit: ChronoUnit): Long = if (unit in SUPPORTED_UNITS) { + values[unit] ?: 0 + } else { + throw InvalidTimeUnitException(unit.name) + } + + /** + * Given a value and [ChronoUnit], replace any stored value for that unit with the given value. + */ + public fun set(value: Long, unit: ChronoUnit) { + when (unit) { + in SUPPORTED_UNITS -> values[unit] = value + + ChronoUnit.WEEKS -> set(value * DAYS_PER_WEEK, ChronoUnit.DAYS) + ChronoUnit.DECADES -> set(value * YEARS_PER_DECADE, ChronoUnit.YEARS) + ChronoUnit.CENTURIES -> set(value * YEARS_PER_CENTURY, ChronoUnit.YEARS) + ChronoUnit.MILLENNIA -> set(value * YEARS_PER_MILLENNIUM, ChronoUnit.YEARS) + + else -> throw InvalidTimeUnitException(unit.name) + } + } + + /** + * Given a [LocalDateTime] (defaulting to `now()`), normalize the stored values by applying them to the datetime + * and calculating their real-world difference. This will replace all stored values with newly-normalized values. + */ + public fun normalize(dateTime: LocalDateTime = LocalDateTime.now()) { + var newDateTime = dateTime + + values.forEach { unit, value -> + newDateTime = newDateTime.plus(value, unit) + } + + val nowTime = LocalDateTime.now() + val futureTime = nowTime.plusHours(get(ChronoUnit.HOURS) % HOURS_PER_DAY) + .plusMinutes(get(ChronoUnit.MINUTES) % MINUTES_PER_HOUR) + .plusSeconds(get(ChronoUnit.SECONDS) % SECONDS_PER_MINUTE) + + val timePeriodDelta = Period.between(nowTime.toLocalDate(), futureTime.toLocalDate()) + + val duration = Duration.between(dateTime, newDateTime) + val period = Period.between(dateTime.toLocalDate(), newDateTime.toLocalDate()) + .minusDays(timePeriodDelta.days.toLong()) + + copyFrom(duration, period) + } + + /** + * Given a [Temporal] subclass, apply the values in this container to it and return the result. + * + * This function will skip units that the [Temporal] doesn't support. + */ + public fun apply(target: T): T { + var result = target + + @Suppress("UNCHECKED_CAST") + values.forEach { unit, value -> + if (target.isSupported(unit)) { + result = result.plus(value, unit) as T + } else { + logger.trace { "Unit $unit is not supported by $target" } + } + } + + return result + } + + /** + * Given a [Duration] and [Period], replace all values in this container with values contained within the passed + * objects. + */ + public fun copyFrom(duration: Duration, period: Period) { + SUPPORTED_UNITS.forEach { unit -> + when (unit) { + ChronoUnit.SECONDS -> values[unit] = duration.toSecondsPart().toLong() + ChronoUnit.MINUTES -> values[unit] = duration.toMinutesPart().toLong() + ChronoUnit.HOURS -> values[unit] = duration.toHoursPart().toLong() + + ChronoUnit.DAYS -> values[unit] = period.days.toLong() + ChronoUnit.MONTHS -> values[unit] = period.months.toLong() + ChronoUnit.YEARS -> values[unit] = period.years.toLong() + + else -> {} + } + } + } + + /** + * Given a [Duration], replace all time-relevant values with values contained within the passed object. + */ + public fun copyFrom(duration: Duration) { + SUPPORTED_UNITS.forEach { unit -> + when (unit) { + ChronoUnit.SECONDS -> values[unit] = duration.toSecondsPart().toLong() + ChronoUnit.MINUTES -> values[unit] = duration.toMinutesPart().toLong() + ChronoUnit.HOURS -> values[unit] = duration.toHoursPart().toLong() + + else -> {} + } + } + } + + /** + * Given a [Period], replace all date-relevant values with values contained within the passed object. + */ + public fun copyFrom(period: Period) { + SUPPORTED_UNITS.forEach { unit -> + when (unit) { + ChronoUnit.DAYS -> values[unit] = period.days.toLong() + ChronoUnit.MONTHS -> values[unit] = period.months.toLong() + ChronoUnit.YEARS -> values[unit] = period.years.toLong() + + else -> {} + } + } + } + + public companion object { + /** + * Create a ChronoUnit and populate it with the values contained in the passed [Duration] and [Period] objects. + */ + public fun of(duration: Duration, period: Period): ChronoContainer { + val container = ChronoContainer() + + container.copyFrom(duration, period) + + return container + } + + /** + * Create a ChronoUnit and populate it with the values contained in the passed [Duration] object. + */ + public fun of(duration: Duration): ChronoContainer { + val container = ChronoContainer() + + container.copyFrom(duration) + + return container + } + + /** + * Create a ChronoUnit and populate it with the values contained in the passed [Period] object. + */ + public fun of(period: Period): ChronoContainer { + val container = ChronoContainer() + + container.copyFrom(period) + + return container + } + + /** + * Given two [LocalDateTime] objects, calculate the difference between them and return a new ChronoUnit, + * pre-populated with that difference. + */ + public fun between(before: LocalDateTime, after: LocalDateTime): ChronoContainer { + val duration = Duration.between(before, after) + val period = Period.between(before.toLocalDate(), after.toLocalDate()) + + return of(duration, period) + } + + /** + * Given two [LocalDate] objects, calculate the difference between them and return a new ChronoUnit, + * pre-populated with that difference. + */ + public fun between(before: LocalDate, after: LocalDate): ChronoContainer { + val period = Period.between(before, after) + + return of(period) + } + + /** + * Given two [LocalTime] objects, calculate the difference between them and return a new ChronoUnit, + * pre-populated with that difference. + */ + public fun between(before: LocalTime, after: LocalTime): ChronoContainer { + val duration = Duration.between(before, after) + + return of(duration) + } + } } diff --git a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/ChronoContainerSerializer.kt b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/ChronoContainerSerializer.kt index 413b7815b3..f47c2408b9 100644 --- a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/ChronoContainerSerializer.kt +++ b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/ChronoContainerSerializer.kt @@ -18,28 +18,28 @@ import java.time.temporal.ChronoUnit * Serializer that converts [ChronoContainer]s between comma-separated `name:amount` string pairs. */ public class ChronoContainerSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("ChronoContainer", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("ChronoContainer", PrimitiveKind.STRING) - override fun deserialize(decoder: Decoder): ChronoContainer { - val container = ChronoContainer() - val string = decoder.decodeString() + override fun deserialize(decoder: Decoder): ChronoContainer { + val container = ChronoContainer() + val string = decoder.decodeString() - string.split(",").forEach { - val (unitName, amount) = it.split(":") - val unit = ChronoUnit.valueOf(unitName) + string.split(",").forEach { + val (unitName, amount) = it.split(":") + val unit = ChronoUnit.valueOf(unitName) - container.plus(amount.toLong(), unit) - } + container.plus(amount.toLong(), unit) + } - return container - } + return container + } - override fun serialize(encoder: Encoder, value: ChronoContainer) { - val string = value.values - .map { (unit, amount) -> "${unit.name}:$amount" } - .joinToString(",") + override fun serialize(encoder: Encoder, value: ChronoContainer) { + val string = value.values + .map { (unit, amount) -> "${unit.name}:$amount" } + .joinToString(",") - encoder.encodeString(string) - } + encoder.encodeString(string) + } } diff --git a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationCoalescingConverter.kt b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationCoalescingConverter.kt index a46d6138d9..e628596801 100644 --- a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationCoalescingConverter.kt +++ b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationCoalescingConverter.kt @@ -40,178 +40,178 @@ import java.time.LocalDateTime * @see parseDurationJ8 */ @Converter( - names = ["j8Duration"], - types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL], - imports = ["java.time.*"], - - builderFields = [ - "public var longHelp: Boolean = true", - "public var positiveOnly: Boolean = true", - ], + names = ["j8Duration"], + types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL], + imports = ["java.time.*"], + + builderFields = [ + "public var longHelp: Boolean = true", + "public var positiveOnly: Boolean = true", + ], ) public class J8DurationCoalescingConverter( - public val longHelp: Boolean = true, - public val positiveOnly: Boolean = true, - shouldThrow: Boolean = false, - override var validator: Validator = null + public val longHelp: Boolean = true, + public val positiveOnly: Boolean = true, + shouldThrow: Boolean = false, + override var validator: Validator = null, ) : CoalescingConverter(shouldThrow) { - override val signatureTypeString: String = "converters.duration.error.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.duration.error.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - private val logger: KLogger = KotlinLogging.logger {} + private val logger: KLogger = KotlinLogging.logger {} - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - val durations: MutableList = mutableListOf() + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + val durations: MutableList = mutableListOf() - val ignoredWords: List = context.translate("utils.durations.ignoredWords") - .split(",") - .toMutableList() - .apply { remove(EMPTY_VALUE_STRING) } - - var skipNext: Boolean = false + val ignoredWords: List = context.translate("utils.durations.ignoredWords") + .split(",") + .toMutableList() + .apply { remove(EMPTY_VALUE_STRING) } + + var skipNext: Boolean = false - val args: List = named ?: parser?.run { - val tokens: MutableList = mutableListOf() + val args: List = named ?: parser?.run { + val tokens: MutableList = mutableListOf() - while (hasNext) { - val nextToken: PositionalArgumentToken? = peekNext() + while (hasNext) { + val nextToken: PositionalArgumentToken? = peekNext() - if (nextToken!!.data.all { J8DurationParser.charValid(it, context.getLocale()) }) { - tokens.add(parseNext()!!.data) - } else { - break - } - } - - tokens - } ?: return 0 + if (nextToken!!.data.all { J8DurationParser.charValid(it, context.getLocale()) }) { + tokens.add(parseNext()!!.data) + } else { + break + } + } + + tokens + } ?: return 0 - @Suppress("LoopWithTooManyJumpStatements") // Well you rewrite it then, detekt - for (index in 0 until args.size) { - if (skipNext) { - skipNext = false + @Suppress("LoopWithTooManyJumpStatements") // Well you rewrite it then, detekt + for (index in 0 until args.size) { + if (skipNext) { + skipNext = false - continue - } + continue + } - val arg: String = args[index] + val arg: String = args[index] - if (arg in ignoredWords) continue + if (arg in ignoredWords) continue - try { - // We do it this way so that we stop parsing as soon as an invalid string is found - J8DurationParser.parse(arg, context.getLocale()) - J8DurationParser.parse(durations.joinToString("") + arg, context.getLocale()) + try { + // We do it this way so that we stop parsing as soon as an invalid string is found + J8DurationParser.parse(arg, context.getLocale()) + J8DurationParser.parse(durations.joinToString("") + arg, context.getLocale()) - durations.add(arg) - } catch (e: DurationParserException) { - try { - val nextIndex: Int = index + 1 + durations.add(arg) + } catch (e: DurationParserException) { + try { + val nextIndex: Int = index + 1 - if (nextIndex >= args.size) { - throw e - } + if (nextIndex >= args.size) { + throw e + } - val nextArg: String = args[nextIndex] - val combined: String = arg + nextArg + val nextArg: String = args[nextIndex] + val combined: String = arg + nextArg - J8DurationParser.parse(combined, context.getLocale()) - J8DurationParser.parse(durations.joinToString("") + combined, context.getLocale()) + J8DurationParser.parse(combined, context.getLocale()) + J8DurationParser.parse(durations.joinToString("") + combined, context.getLocale()) - durations.add(combined) - skipNext = true - } catch (t: InvalidTimeUnitException) { - throwIfNecessary(t, context) + durations.add(combined) + skipNext = true + } catch (t: InvalidTimeUnitException) { + throwIfNecessary(t, context) - break - } catch (t: DurationParserException) { - throwIfNecessary(t, context) + break + } catch (t: DurationParserException) { + throwIfNecessary(t, context) - break - } - } - } + break + } + } + } - try { - val result: ChronoContainer = J8DurationParser.parse( - durations.joinToString(""), - context.getLocale() - ) + try { + val result: ChronoContainer = J8DurationParser.parse( + durations.joinToString(""), + context.getLocale() + ) - if (positiveOnly) { - val normalized: ChronoContainer = result.clone() + if (positiveOnly) { + val normalized: ChronoContainer = result.clone() - normalized.normalize(LocalDateTime.now()) + normalized.normalize(LocalDateTime.now()) - if (!normalized.isPositive()) { - throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) - } - } + if (!normalized.isPositive()) { + throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) + } + } - parsed = result - } catch (e: InvalidTimeUnitException) { - throwIfNecessary(e, context, true) - } catch (e: DurationParserException) { - throwIfNecessary(e, context, true) - } + parsed = result + } catch (e: InvalidTimeUnitException) { + throwIfNecessary(e, context, true) + } catch (e: DurationParserException) { + throwIfNecessary(e, context, true) + } - return durations.size - } + return durations.size + } - private suspend fun throwIfNecessary( - e: Exception, - context: CommandContext, - override: Boolean = false - ): Unit = if (shouldThrow || override) { - when (e) { - is InvalidTimeUnitException -> { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - - throw DiscordRelayedException(message) - } - - is DurationParserException -> throw DiscordRelayedException(e.error) - - else -> throw e - } - } else { - logger.debug(e) { "Error thrown during duration parsing" } - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val arg: String = (option as? StringOptionValue)?.value ?: return false - - try { - val result: ChronoContainer = J8DurationParser.parse(arg, context.getLocale()) - - if (positiveOnly) { - val normalized: ChronoContainer = result.clone() - - normalized.normalize(LocalDateTime.now()) - - if (!normalized.isPositive()) { - throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) - } - } - - parsed = result - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } - - return true - } + private suspend fun throwIfNecessary( + e: Exception, + context: CommandContext, + override: Boolean = false, + ): Unit = if (shouldThrow || override) { + when (e) { + is InvalidTimeUnitException -> { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + + throw DiscordRelayedException(message) + } + + is DurationParserException -> throw DiscordRelayedException(e.error) + + else -> throw e + } + } else { + logger.debug(e) { "Error thrown during duration parsing" } + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val arg: String = (option as? StringOptionValue)?.value ?: return false + + try { + val result: ChronoContainer = J8DurationParser.parse(arg, context.getLocale()) + + if (positiveOnly) { + val normalized: ChronoContainer = result.clone() + + normalized.normalize(LocalDateTime.now()) + + if (!normalized.isPositive()) { + throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) + } + } + + parsed = result + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } + + return true + } } diff --git a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationConverter.kt b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationConverter.kt index 3cb282e2b7..022d9afc30 100644 --- a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationConverter.kt +++ b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationConverter.kt @@ -33,85 +33,85 @@ import java.time.LocalDateTime * @param positiveOnly Whether a positive duration is required - `true` by default. */ @Converter( - names = ["j8Duration"], - types = [ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE], - imports = ["java.time.*"], - - builderFields = [ - "public var longHelp: Boolean = true", - "public var positiveOnly: Boolean = true", - ], + names = ["j8Duration"], + types = [ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE], + imports = ["java.time.*"], + + builderFields = [ + "public var longHelp: Boolean = true", + "public var positiveOnly: Boolean = true", + ], ) public class J8DurationConverter( - public val longHelp: Boolean = true, - public val positiveOnly: Boolean = true, - override var validator: Validator = null + public val longHelp: Boolean = true, + public val positiveOnly: Boolean = true, + override var validator: Validator = null, ) : SingleConverter() { - override val signatureTypeString: String = "converters.duration.error.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.duration.error.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - try { - val result: ChronoContainer = J8DurationParser.parse(arg, context.getLocale()) + try { + val result: ChronoContainer = J8DurationParser.parse(arg, context.getLocale()) - if (positiveOnly) { - val normalized: ChronoContainer = result.clone() + if (positiveOnly) { + val normalized: ChronoContainer = result.clone() - normalized.normalize(LocalDateTime.now()) + normalized.normalize(LocalDateTime.now()) - if (!normalized.isPositive()) { - throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) - } - } + if (!normalized.isPositive()) { + throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) + } + } - parsed = result - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + parsed = result + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val arg: String = (option as? StringOptionValue)?.value ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val arg: String = (option as? StringOptionValue)?.value ?: return false - try { - val result: ChronoContainer = J8DurationParser.parse(arg, context.getLocale()) + try { + val result: ChronoContainer = J8DurationParser.parse(arg, context.getLocale()) - if (positiveOnly) { - val normalized: ChronoContainer = result.clone() + if (positiveOnly) { + val normalized: ChronoContainer = result.clone() - normalized.normalize(LocalDateTime.now()) + normalized.normalize(LocalDateTime.now()) - if (!normalized.isPositive()) { - throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) - } - } + if (!normalized.isPositive()) { + throw DiscordRelayedException(context.translate("converters.duration.error.positiveOnly")) + } + } - parsed = result - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + parsed = result + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } - return true - } + return true + } } diff --git a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationParser.kt b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationParser.kt index ba1e0560ca..b6412a0baa 100644 --- a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationParser.kt +++ b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8DurationParser.kt @@ -18,57 +18,57 @@ import java.util.* * Object in charge of parsing strings into [ChronoContainer]s, using translated locale-aware units. */ public object J8DurationParser : KordExKoinComponent { - private val translations: TranslationsProvider by inject() + private val translations: TranslationsProvider by inject() - /** Check whether the given character is a valid duration unit character. **/ - public fun charValid(char: Char, locale: Locale): Boolean = - char.isDigit() || - char == ' ' || - J8TimeUnitCache.getUnits(locale).filterKeys { it.startsWith(char) }.isNotEmpty() + /** Check whether the given character is a valid duration unit character. **/ + public fun charValid(char: Char, locale: Locale): Boolean = + char.isDigit() || + char == ' ' || + J8TimeUnitCache.getUnits(locale).filterKeys { it.startsWith(char) }.isNotEmpty() - /** - * Parse the provided string to a [ChronoContainer] object, using the strings provided by the given [Locale]. - */ - public fun parse(input: String, locale: Locale): ChronoContainer { - val unitMap = J8TimeUnitCache.getUnits(locale) + /** + * Parse the provided string to a [ChronoContainer] object, using the strings provided by the given [Locale]. + */ + public fun parse(input: String, locale: Locale): ChronoContainer { + val unitMap = J8TimeUnitCache.getUnits(locale) - val units: MutableList = mutableListOf() - val values: MutableList = mutableListOf() + val units: MutableList = mutableListOf() + val values: MutableList = mutableListOf() - var buffer = input.replace(",", "") - .replace("+", "") - .replace(" ", "") + var buffer = input.replace(",", "") + .replace("+", "") + .replace(" ", "") - val container = ChronoContainer() + val container = ChronoContainer() - while (buffer.isNotEmpty()) { - if (isValueChar(buffer.first())) { - val (value, remaining) = buffer.splitOn(J8DurationParser::isNotValueChar) + while (buffer.isNotEmpty()) { + if (isValueChar(buffer.first())) { + val (value, remaining) = buffer.splitOn(J8DurationParser::isNotValueChar) - values.add(value) - buffer = remaining - } else { - val (unit, remaining) = buffer.splitOn(J8DurationParser::isValueChar) + values.add(value) + buffer = remaining + } else { + val (unit, remaining) = buffer.splitOn(J8DurationParser::isValueChar) - units.add(unit) - buffer = remaining - } - } + units.add(unit) + buffer = remaining + } + } - if (values.size != units.size) { - throw DurationParserException(translations.translate("converters.duration.error.badUnitPairs", locale)) - } + if (values.size != units.size) { + throw DurationParserException(translations.translate("converters.duration.error.badUnitPairs", locale)) + } - while (units.isNotEmpty()) { - val (unitString, valueString) = units.removeFirst() to values.removeFirst() - val timeUnit = unitMap[unitString.lowercase()] ?: throw InvalidTimeUnitException(unitString) + while (units.isNotEmpty()) { + val (unitString, valueString) = units.removeFirst() to values.removeFirst() + val timeUnit = unitMap[unitString.lowercase()] ?: throw InvalidTimeUnitException(unitString) - container.plus(valueString.toLong(), timeUnit) - } + container.plus(valueString.toLong(), timeUnit) + } - return container - } + return container + } - private fun isValueChar(char: Char) = char.isDigit() || char == '-' - private fun isNotValueChar(char: Char) = !isValueChar(char) + private fun isValueChar(char: Char) = char.isDigit() || char == '-' + private fun isNotValueChar(char: Char) = !isValueChar(char) } diff --git a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8TimeUnitCache.kt b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8TimeUnitCache.kt index 2d8ba9e9b5..daa037f681 100644 --- a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8TimeUnitCache.kt +++ b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/J8TimeUnitCache.kt @@ -15,38 +15,38 @@ import java.util.* private typealias UnitMap = LinkedHashMap private val keyMap: UnitMap = linkedMapOf( - "utils.units.second" to ChronoUnit.SECONDS, - "utils.units.minute" to ChronoUnit.MINUTES, - "utils.units.hour" to ChronoUnit.HOURS, - "utils.units.day" to ChronoUnit.DAYS, - "utils.units.week" to ChronoUnit.WEEKS, - "utils.units.month" to ChronoUnit.MONTHS, - "utils.units.year" to ChronoUnit.YEARS, + "utils.units.second" to ChronoUnit.SECONDS, + "utils.units.minute" to ChronoUnit.MINUTES, + "utils.units.hour" to ChronoUnit.HOURS, + "utils.units.day" to ChronoUnit.DAYS, + "utils.units.week" to ChronoUnit.WEEKS, + "utils.units.month" to ChronoUnit.MONTHS, + "utils.units.year" to ChronoUnit.YEARS, ) /** * Simple object that caches translated time units per locale. */ public object J8TimeUnitCache : KordExKoinComponent { - private val translations: TranslationsProvider by inject() - private val valueCache: MutableMap = mutableMapOf() + private val translations: TranslationsProvider by inject() + private val valueCache: MutableMap = mutableMapOf() - /** Return a mapping of all translated unit names to ChronoUnit objects, based on the given locale. **/ - public fun getUnits(locale: Locale): UnitMap { - if (valueCache[locale] == null) { - val unitMap: UnitMap = linkedMapOf() + /** Return a mapping of all translated unit names to ChronoUnit objects, based on the given locale. **/ + public fun getUnits(locale: Locale): UnitMap { + if (valueCache[locale] == null) { + val unitMap: UnitMap = linkedMapOf() - keyMap.forEach { key, value -> - val result = translations.translate(key, locale) + keyMap.forEach { key, value -> + val result = translations.translate(key, locale) - result.split(",").map { it.trim() }.forEach { - unitMap[it] = value - } - } + result.split(",").map { it.trim() }.forEach { + unitMap[it] = value + } + } - valueCache[locale] = unitMap - } + valueCache[locale] = unitMap + } - return valueCache[locale]!! - } + return valueCache[locale]!! + } } diff --git a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/Utils.kt b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/Utils.kt index f4b0734c3e..80cb4574b1 100644 --- a/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/Utils.kt +++ b/modules/java-time/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/java/Utils.kt @@ -24,36 +24,36 @@ private const val DAYS_PER_WEEK = 7L */ @Throws(IllegalArgumentException::class) public fun formatChronoContainer( - container: ChronoContainer, - locale: Locale, - relativeTo: LocalDateTime = LocalDateTime.now() + container: ChronoContainer, + locale: Locale, + relativeTo: LocalDateTime = LocalDateTime.now(), ): String? { - container.normalize(relativeTo) + container.normalize(relativeTo) - val years = container.get(ChronoUnit.YEARS) - val months = container.get(ChronoUnit.MONTHS) - val days = container.get(ChronoUnit.DAYS) % DAYS_PER_WEEK - val weeks = container.get(ChronoUnit.DAYS) / DAYS_PER_WEEK + val years = container.get(ChronoUnit.YEARS) + val months = container.get(ChronoUnit.MONTHS) + val days = container.get(ChronoUnit.DAYS) % DAYS_PER_WEEK + val weeks = container.get(ChronoUnit.DAYS) / DAYS_PER_WEEK - val hours = container.get(ChronoUnit.HOURS) - val minutes = container.get(ChronoUnit.MINUTES) - val seconds = container.get(ChronoUnit.SECONDS) + val hours = container.get(ChronoUnit.HOURS) + val minutes = container.get(ChronoUnit.MINUTES) + val seconds = container.get(ChronoUnit.SECONDS) - val fmt = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE) - val measures: MutableList = mutableListOf() + val fmt = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE) + val measures: MutableList = mutableListOf() - if (years != 0L) measures.add(Measure(years, MeasureUnit.YEAR)) - if (months != 0L) measures.add(Measure(months, MeasureUnit.MONTH)) - if (weeks != 0L) measures.add(Measure(weeks, MeasureUnit.WEEK)) - if (days != 0L) measures.add(Measure(days, MeasureUnit.DAY)) - if (hours != 0L) measures.add(Measure(hours, MeasureUnit.HOUR)) - if (minutes != 0L) measures.add(Measure(minutes, MeasureUnit.MINUTE)) - if (seconds != 0L) measures.add(Measure(seconds, MeasureUnit.SECOND)) + if (years != 0L) measures.add(Measure(years, MeasureUnit.YEAR)) + if (months != 0L) measures.add(Measure(months, MeasureUnit.MONTH)) + if (weeks != 0L) measures.add(Measure(weeks, MeasureUnit.WEEK)) + if (days != 0L) measures.add(Measure(days, MeasureUnit.DAY)) + if (hours != 0L) measures.add(Measure(hours, MeasureUnit.HOUR)) + if (minutes != 0L) measures.add(Measure(minutes, MeasureUnit.MINUTE)) + if (seconds != 0L) measures.add(Measure(seconds, MeasureUnit.SECOND)) - if (measures.isEmpty()) return null + if (measures.isEmpty()) return null - @Suppress("SpreadOperator") // There's no other way, really - return fmt.formatMeasures(*measures.toTypedArray()) + @Suppress("SpreadOperator") // There's no other way, really + return fmt.formatMeasures(*measures.toTypedArray()) } /** @@ -62,8 +62,8 @@ public fun formatChronoContainer( * The string is intended to be readable for humans - "a days, b hours, c minutes, d seconds". */ public fun ChronoContainer.toHuman( - locale: Locale, - relativeTo: LocalDateTime = LocalDateTime.now() + locale: Locale, + relativeTo: LocalDateTime = LocalDateTime.now(), ): String? = formatChronoContainer(this, locale, relativeTo) /** @@ -72,8 +72,8 @@ public fun ChronoContainer.toHuman( * The string is intended to be readable for humans - "a days, b hours, c minutes, d seconds". */ public suspend fun ChronoContainer.toHuman( - context: CommandContext, - relativeTo: LocalDateTime = LocalDateTime.now() + context: CommandContext, + relativeTo: LocalDateTime = LocalDateTime.now(), ): String? = toHuman(context.getLocale(), relativeTo) /** @@ -81,4 +81,4 @@ public suspend fun ChronoContainer.toHuman( * you can include in your messages, which Discord should automatically format for users based on their locale. */ public fun Instant.toDiscord(format: TimestampType = TimestampType.Default): String = - format.format(epochSecond) + format.format(epochSecond) diff --git a/modules/java-time/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt b/modules/java-time/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt index 4b73e150de..80f6f23f42 100644 --- a/modules/java-time/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt +++ b/modules/java-time/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt @@ -15,40 +15,40 @@ import org.koin.core.logger.Level val TEST_SERVER_ID = Snowflake(787452339908116521UL) suspend fun main() { - val bot = ExtensibleBot(env("TOKEN")) { - koinLogLevel = Level.DEBUG - - i18n { - localeResolver { _, _, user, _ -> - @Suppress("UnderscoresInNumericLiterals") - when (user?.id?.value) { - 560515299388948500UL -> SupportedLocales.FINNISH - 242043299022635020UL -> SupportedLocales.FRENCH - 407110650217627658UL -> SupportedLocales.FRENCH - 667552017434017794UL -> SupportedLocales.CHINESE_SIMPLIFIED - 185461862878543872UL -> SupportedLocales.GERMAN - - else -> defaultLocale - } - } - } - - chatCommands { - defaultPrefix = "?" - - prefix { default -> - if (guildId == TEST_SERVER_ID) { - "!" - } else { - default // "?" - } - } - } - - extensions { - add(::TestExtension) - } - } - - bot.start() + val bot = ExtensibleBot(env("TOKEN")) { + koinLogLevel = Level.DEBUG + + i18n { + localeResolver { _, _, user, _ -> + @Suppress("UnderscoresInNumericLiterals") + when (user?.id?.value) { + 560515299388948500UL -> SupportedLocales.FINNISH + 242043299022635020UL -> SupportedLocales.FRENCH + 407110650217627658UL -> SupportedLocales.FRENCH + 667552017434017794UL -> SupportedLocales.CHINESE_SIMPLIFIED + 185461862878543872UL -> SupportedLocales.GERMAN + + else -> defaultLocale + } + } + } + + chatCommands { + defaultPrefix = "?" + + prefix { default -> + if (guildId == TEST_SERVER_ID) { + "!" + } else { + default // "?" + } + } + } + + extensions { + add(::TestExtension) + } + } + + bot.start() } diff --git a/modules/java-time/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/TestExtension.kt b/modules/java-time/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/TestExtension.kt index cbbe69705e..4da2f05b36 100644 --- a/modules/java-time/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/TestExtension.kt +++ b/modules/java-time/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/TestExtension.kt @@ -16,25 +16,25 @@ import com.kotlindiscord.kord.extensions.utils.respond // They're IDs @Suppress("UnderscoresInNumericLiterals") class TestExtension : Extension() { - override val name = "test" + override val name = "test" - class TestArgs : Arguments() { - val duration by coalescingJ8Duration { - name = "duration" - description = "Duration argument" - } - } + class TestArgs : Arguments() { + val duration by coalescingJ8Duration { + name = "duration" + description = "Duration argument" + } + } - override suspend fun setup() { - chatCommand(::TestArgs) { - name = "format" - description = "Let's test formatting." + override suspend fun setup() { + chatCommand(::TestArgs) { + name = "format" + description = "Let's test formatting." - action { - message.respond( - arguments.duration.toHuman(this) ?: "Empty duration!" - ) - } - } - } + action { + message.respond( + arguments.duration.toHuman(this) ?: "Empty duration!" + ) + } + } + } } diff --git a/modules/java-time/src/test/resources/logback.groovy b/modules/java-time/src/test/resources/logback.groovy index 76c3dc6373..3c126a7dee 100644 --- a/modules/java-time/src/test/resources/logback.groovy +++ b/modules/java-time/src/test/resources/logback.groovy @@ -11,30 +11,30 @@ def environment = System.getenv().getOrDefault("ENVIRONMENT", "production") def defaultLevel = TRACE if (environment == "spam") { - statusListener(OnConsoleStatusListener) + statusListener(OnConsoleStatusListener) - logger("dev.kord.rest.DefaultGateway", TRACE) + logger("dev.kord.rest.DefaultGateway", TRACE) } else { - // Silence warning about missing native PRNG - logger("io.ktor.util.random", ERROR) + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) } appender("CONSOLE", ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" - withJansi = true - } + withJansi = true + } - target = ConsoleTarget.SystemOut + target = ConsoleTarget.SystemOut } appender("FILE", FileAppender) { - file = "output.log" + file = "output.log" - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } } root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/modules/time4j/build.gradle.kts b/modules/time4j/build.gradle.kts index fee8cd433b..0d883894c6 100644 --- a/modules/time4j/build.gradle.kts +++ b/modules/time4j/build.gradle.kts @@ -1,35 +1,35 @@ plugins { - `kordex-module` - `published-module` - `ksp-module` + `kordex-module` + `published-module` + `ksp-module` } metadata { - name = "KordEx: Time4J" - description = "KordEx module that provides converters that support Time4J" + name = "KordEx: Time4J" + description = "KordEx module that provides converters that support Time4J" } dependencies { - api(libs.time4j.base) - api(libs.time4j.tzdata) + api(libs.time4j.base) + api(libs.time4j.tzdata) - implementation(libs.kotlin.stdlib) + implementation(libs.kotlin.stdlib) - implementation(project(":kord-extensions")) - implementation(project(":annotations")) + implementation(project(":kord-extensions")) + implementation(project(":annotations")) - ksp(project(":annotation-processor")) + ksp(project(":annotation-processor")) - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - testImplementation(libs.groovy) // For logback config - testImplementation(libs.jansi) - testImplementation(libs.junit) - testImplementation(libs.logback) - testImplementation(libs.logback.groovy) + testImplementation(libs.groovy) // For logback config + testImplementation(libs.jansi) + testImplementation(libs.junit) + testImplementation(libs.logback) + testImplementation(libs.logback.groovy) } dokkaModule { - moduleName.set("Kord Extensions: Time4J") + moduleName.set("Kord Extensions: Time4J") } diff --git a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationCoalescingConverter.kt b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationCoalescingConverter.kt index 415bca8800..bdcd286ed8 100644 --- a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationCoalescingConverter.kt +++ b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationCoalescingConverter.kt @@ -39,152 +39,152 @@ import net.time4j.IsoUnit * @see parseT4JDuration */ @Converter( - names = ["t4JDuration"], - types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL], - imports = ["net.time4j.*"], + names = ["t4JDuration"], + types = [ConverterType.COALESCING, ConverterType.DEFAULTING, ConverterType.OPTIONAL], + imports = ["net.time4j.*"], - builderFields = [ - "public var longHelp: Boolean = true", - ], + builderFields = [ + "public var longHelp: Boolean = true", + ], ) public class T4JDurationCoalescingConverter( - public val longHelp: Boolean = true, - shouldThrow: Boolean = false, - override var validator: Validator> = null + public val longHelp: Boolean = true, + shouldThrow: Boolean = false, + override var validator: Validator> = null, ) : CoalescingConverter>(shouldThrow) { - override val signatureTypeString: String = "converters.duration.error.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE - - private val logger: KLogger = KotlinLogging.logger {} - - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - val durations: MutableList = mutableListOf() - - val ignoredWords: List = context.translate("utils.durations.ignoredWords") - .split(",") - .toMutableList() - .apply { remove(EMPTY_VALUE_STRING) } - - var skipNext: Boolean = false - - val args: List = named ?: parser?.run { - val tokens: MutableList = mutableListOf() - - while (hasNext) { - val nextToken: PositionalArgumentToken? = peekNext() - - if (nextToken!!.data.all { T4JDurationParser.charValid(it, context.getLocale()) }) { - tokens.add(parseNext()!!.data) - } else { - break - } - } - - tokens - } ?: return 0 - - @Suppress("LoopWithTooManyJumpStatements") // Well you rewrite it then, detekt - for (index in args.indices) { - if (skipNext) { - skipNext = false - - continue - } - - val arg: String = args[index] - - if (arg in ignoredWords) continue - - try { - // We do it this way so that we stop parsing as soon as an invalid string is found - T4JDurationParser.parse(arg, context.getLocale()) - T4JDurationParser.parse(durations.joinToString("") + arg, context.getLocale()) - - durations.add(arg) - } catch (e: DurationParserException) { - try { - val nextIndex: Int = index + 1 - - if (nextIndex >= args.size) { - throw e - } - - val nextArg: String = args[nextIndex] - val combined: String = arg + nextArg - - T4JDurationParser.parse(combined, context.getLocale()) - T4JDurationParser.parse(durations.joinToString("") + combined, context.getLocale()) - - durations.add(combined) - skipNext = true - } catch (t: InvalidTimeUnitException) { - throwIfNecessary(t, context) - - break - } catch (t: DurationParserException) { - throwIfNecessary(t, context) - - break - } - } - } - - try { - parsed = T4JDurationParser.parse( - durations.joinToString(""), - context.getLocale() - ) - } catch (e: InvalidTimeUnitException) { - throwIfNecessary(e, context, true) - } catch (e: DurationParserException) { - throwIfNecessary(e, context, true) - } - - return durations.size - } - - private suspend fun throwIfNecessary( - e: Exception, - context: CommandContext, - override: Boolean = false - ): Unit = if (shouldThrow || override) { - when (e) { - is InvalidTimeUnitException -> { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - - throw DiscordRelayedException(message) - } - - is DurationParserException -> throw DiscordRelayedException(e.error) - - else -> throw e - } - } else { - logger.debug(e) { "Error thrown during duration parsing" } - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val arg: String = (option as? StringOptionValue)?.value ?: return false - - try { - this.parsed = T4JDurationParser.parse(arg, context.getLocale()) - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } - - return true - } + override val signatureTypeString: String = "converters.duration.error.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE + + private val logger: KLogger = KotlinLogging.logger {} + + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + val durations: MutableList = mutableListOf() + + val ignoredWords: List = context.translate("utils.durations.ignoredWords") + .split(",") + .toMutableList() + .apply { remove(EMPTY_VALUE_STRING) } + + var skipNext: Boolean = false + + val args: List = named ?: parser?.run { + val tokens: MutableList = mutableListOf() + + while (hasNext) { + val nextToken: PositionalArgumentToken? = peekNext() + + if (nextToken!!.data.all { T4JDurationParser.charValid(it, context.getLocale()) }) { + tokens.add(parseNext()!!.data) + } else { + break + } + } + + tokens + } ?: return 0 + + @Suppress("LoopWithTooManyJumpStatements") // Well you rewrite it then, detekt + for (index in args.indices) { + if (skipNext) { + skipNext = false + + continue + } + + val arg: String = args[index] + + if (arg in ignoredWords) continue + + try { + // We do it this way so that we stop parsing as soon as an invalid string is found + T4JDurationParser.parse(arg, context.getLocale()) + T4JDurationParser.parse(durations.joinToString("") + arg, context.getLocale()) + + durations.add(arg) + } catch (e: DurationParserException) { + try { + val nextIndex: Int = index + 1 + + if (nextIndex >= args.size) { + throw e + } + + val nextArg: String = args[nextIndex] + val combined: String = arg + nextArg + + T4JDurationParser.parse(combined, context.getLocale()) + T4JDurationParser.parse(durations.joinToString("") + combined, context.getLocale()) + + durations.add(combined) + skipNext = true + } catch (t: InvalidTimeUnitException) { + throwIfNecessary(t, context) + + break + } catch (t: DurationParserException) { + throwIfNecessary(t, context) + + break + } + } + } + + try { + parsed = T4JDurationParser.parse( + durations.joinToString(""), + context.getLocale() + ) + } catch (e: InvalidTimeUnitException) { + throwIfNecessary(e, context, true) + } catch (e: DurationParserException) { + throwIfNecessary(e, context, true) + } + + return durations.size + } + + private suspend fun throwIfNecessary( + e: Exception, + context: CommandContext, + override: Boolean = false, + ): Unit = if (shouldThrow || override) { + when (e) { + is InvalidTimeUnitException -> { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + + throw DiscordRelayedException(message) + } + + is DurationParserException -> throw DiscordRelayedException(e.error) + + else -> throw e + } + } else { + logger.debug(e) { "Error thrown during duration parsing" } + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val arg: String = (option as? StringOptionValue)?.value ?: return false + + try { + this.parsed = T4JDurationParser.parse(arg, context.getLocale()) + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } + + return true + } } diff --git a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationConverter.kt b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationConverter.kt index 28c9fcb68a..fabae99bb1 100644 --- a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationConverter.kt +++ b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationConverter.kt @@ -35,59 +35,59 @@ import net.time4j.IsoUnit * @see parseT4JDuration */ @Converter( - names = ["t4JDuration"], - types = [ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE], - imports = ["net.time4j.*"], + names = ["t4JDuration"], + types = [ConverterType.DEFAULTING, ConverterType.OPTIONAL, ConverterType.SINGLE], + imports = ["net.time4j.*"], - builderFields = [ - "public var longHelp: Boolean = true", - ], + builderFields = [ + "public var longHelp: Boolean = true", + ], ) public class T4JDurationConverter( - public val longHelp: Boolean = true, - override var validator: Validator> = null + public val longHelp: Boolean = true, + override var validator: Validator> = null, ) : SingleConverter>() { - override val signatureTypeString: String = "converters.duration.error.signatureType" - override val bundle: String = DEFAULT_KORDEX_BUNDLE + override val signatureTypeString: String = "converters.duration.error.signatureType" + override val bundle: String = DEFAULT_KORDEX_BUNDLE - override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { - val arg: String = named ?: parser?.parseNext()?.data ?: return false + override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean { + val arg: String = named ?: parser?.parseNext()?.data ?: return false - try { - this.parsed = T4JDurationParser.parse(arg, context.getLocale()) - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + try { + this.parsed = T4JDurationParser.parse(arg, context.getLocale()) + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } - return true - } + return true + } - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - val arg: String = (option as? StringOptionValue)?.value ?: return false + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + val arg: String = (option as? StringOptionValue)?.value ?: return false - try { - this.parsed = T4JDurationParser.parse(arg, context.getLocale()) - } catch (e: InvalidTimeUnitException) { - val message: String = context.translate( - "converters.duration.error.invalidUnit", - replacements = arrayOf(e.unit) - ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" + try { + this.parsed = T4JDurationParser.parse(arg, context.getLocale()) + } catch (e: InvalidTimeUnitException) { + val message: String = context.translate( + "converters.duration.error.invalidUnit", + replacements = arrayOf(e.unit) + ) + if (longHelp) "\n\n" + context.translate("converters.duration.help") else "" - throw DiscordRelayedException(message) - } catch (e: DurationParserException) { - throw DiscordRelayedException(e.error) - } + throw DiscordRelayedException(message) + } catch (e: DurationParserException) { + throw DiscordRelayedException(e.error) + } - return true - } + return true + } } diff --git a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationParser.kt b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationParser.kt index b3c8e5516d..ff33d44de6 100644 --- a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationParser.kt +++ b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JDurationParser.kt @@ -20,63 +20,63 @@ import java.util.* * Object in charge of parsing strings into [Duration]s, using translated locale-aware units. */ public object T4JDurationParser : KordExKoinComponent { - private val translations: TranslationsProvider by inject() + private val translations: TranslationsProvider by inject() - /** Check whether the given character is a valid duration unit character. **/ - public fun charValid(char: Char, locale: Locale): Boolean = - char.isDigit() || - char == ' ' || - T4JTimeUnitCache.getUnits(locale).filterKeys { it.startsWith(char) }.isNotEmpty() + /** Check whether the given character is a valid duration unit character. **/ + public fun charValid(char: Char, locale: Locale): Boolean = + char.isDigit() || + char == ' ' || + T4JTimeUnitCache.getUnits(locale).filterKeys { it.startsWith(char) }.isNotEmpty() - /** - * Parse the provided string to a [Duration] object, using the strings provided by the given [Locale]. - */ - public fun parse(input: String, locale: Locale): Duration { - if ("-" in input) { - throw DurationParserException( - translations.translate("converters.duration.error.negativeUnsupported", locale) - ) - } + /** + * Parse the provided string to a [Duration] object, using the strings provided by the given [Locale]. + */ + public fun parse(input: String, locale: Locale): Duration { + if ("-" in input) { + throw DurationParserException( + translations.translate("converters.duration.error.negativeUnsupported", locale) + ) + } - val unitMap = T4JTimeUnitCache.getUnits(locale) + val unitMap = T4JTimeUnitCache.getUnits(locale) - val units: MutableList = mutableListOf() - val values: MutableList = mutableListOf() + val units: MutableList = mutableListOf() + val values: MutableList = mutableListOf() - var buffer = input.replace(",", "") - .replace("+", "") - .replace(" ", "") + var buffer = input.replace(",", "") + .replace("+", "") + .replace(" ", "") - var duration = Duration.ofZero() + var duration = Duration.ofZero() - while (buffer.isNotEmpty()) { - if (isValueChar(buffer.first())) { - val (value, remaining) = buffer.splitOn(::isNotValueChar) + while (buffer.isNotEmpty()) { + if (isValueChar(buffer.first())) { + val (value, remaining) = buffer.splitOn(::isNotValueChar) - values.add(value) - buffer = remaining - } else { - val (unit, remaining) = buffer.splitOn(::isValueChar) + values.add(value) + buffer = remaining + } else { + val (unit, remaining) = buffer.splitOn(::isValueChar) - units.add(unit) - buffer = remaining - } - } + units.add(unit) + buffer = remaining + } + } - if (values.size != units.size) { - throw DurationParserException(translations.translate("converters.duration.error.badUnitPairs", locale)) - } + if (values.size != units.size) { + throw DurationParserException(translations.translate("converters.duration.error.badUnitPairs", locale)) + } - while (units.isNotEmpty()) { - val (unitString, valueString) = units.removeFirst() to values.removeFirst() - val timeUnit = unitMap[unitString.lowercase()] ?: throw InvalidTimeUnitException(unitString) + while (units.isNotEmpty()) { + val (unitString, valueString) = units.removeFirst() to values.removeFirst() + val timeUnit = unitMap[unitString.lowercase()] ?: throw InvalidTimeUnitException(unitString) - duration = duration.plus(valueString.toLong(), timeUnit) - } + duration = duration.plus(valueString.toLong(), timeUnit) + } - return duration - } + return duration + } - private fun isValueChar(char: Char) = char.isDigit() || char == '-' - private fun isNotValueChar(char: Char) = !isValueChar(char) + private fun isValueChar(char: Char) = char.isDigit() || char == '-' + private fun isNotValueChar(char: Char) = !isValueChar(char) } diff --git a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JTimeUnitCache.kt b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JTimeUnitCache.kt index 80e10264c1..4eae6e11e6 100644 --- a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JTimeUnitCache.kt +++ b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/T4JTimeUnitCache.kt @@ -17,38 +17,38 @@ import java.util.* private typealias UnitMap = LinkedHashMap private val keyMap: UnitMap = linkedMapOf( - "utils.units.second" to ClockUnit.SECONDS, - "utils.units.minute" to ClockUnit.MINUTES, - "utils.units.hour" to ClockUnit.HOURS, - "utils.units.day" to CalendarUnit.DAYS, - "utils.units.week" to CalendarUnit.WEEKS, - "utils.units.month" to CalendarUnit.MONTHS, - "utils.units.year" to CalendarUnit.YEARS, + "utils.units.second" to ClockUnit.SECONDS, + "utils.units.minute" to ClockUnit.MINUTES, + "utils.units.hour" to ClockUnit.HOURS, + "utils.units.day" to CalendarUnit.DAYS, + "utils.units.week" to CalendarUnit.WEEKS, + "utils.units.month" to CalendarUnit.MONTHS, + "utils.units.year" to CalendarUnit.YEARS, ) /** * Simple object that caches translated time units per locale. */ public object T4JTimeUnitCache : KordExKoinComponent { - private val translations: TranslationsProvider by inject() - private val valueCache: MutableMap = mutableMapOf() + private val translations: TranslationsProvider by inject() + private val valueCache: MutableMap = mutableMapOf() - /** Return a mapping of all translated unit names to ChronoUnit objects, based on the given locale. **/ - public fun getUnits(locale: Locale): UnitMap { - if (valueCache[locale] == null) { - val unitMap: UnitMap = linkedMapOf() + /** Return a mapping of all translated unit names to ChronoUnit objects, based on the given locale. **/ + public fun getUnits(locale: Locale): UnitMap { + if (valueCache[locale] == null) { + val unitMap: UnitMap = linkedMapOf() - keyMap.forEach { key, value -> - val result = translations.translate(key, locale) + keyMap.forEach { key, value -> + val result = translations.translate(key, locale) - result.split(",").map { it.trim() }.forEach { - unitMap[it] = value - } - } + result.split(",").map { it.trim() }.forEach { + unitMap[it] = value + } + } - valueCache[locale] = unitMap - } + valueCache[locale] = unitMap + } - return valueCache[locale]!! - } + return valueCache[locale]!! + } } diff --git a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/Utils.kt b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/Utils.kt index 4739971430..46f0fac3e0 100644 --- a/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/Utils.kt +++ b/modules/time4j/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/time/time4j/Utils.kt @@ -26,44 +26,44 @@ private const val DAYS_PER_WEEK = 7L @Suppress("DestructuringDeclarationWithTooManyEntries") @Throws(IllegalStateException::class) public fun formatT4JDuration( - duration: Duration, - locale: Locale, - relativeTo: PlainTimestamp = PlainTimestamp.nowInSystemTime() + duration: Duration, + locale: Locale, + relativeTo: PlainTimestamp = PlainTimestamp.nowInSystemTime(), ): String? { - // This function is pretty cursed, but then again, Time4J is pretty cursed. - val formatter = Duration.Formatter.ofPattern("#################Y::#M::#D::#h::#m::#s") - val offsetTime = duration.addTo(PlainTimestamp.nowInSystemTime()) - - val newDuration = Duration.`in`( - CalendarUnit.YEARS, - CalendarUnit.MONTHS, - CalendarUnit.DAYS, - ClockUnit.HOURS, - ClockUnit.MINUTES, - ClockUnit.SECONDS - ).between(relativeTo, offsetTime).toTemporalAmount() - - val times = formatter.format(newDuration).split("::") - val (years, months, daysTotal, hours, minutes, seconds) = times.map { it.toLong() } - - val days = daysTotal % DAYS_PER_WEEK - val weeks = daysTotal / DAYS_PER_WEEK - - val fmt = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE) - val measures: MutableList = mutableListOf() - - if (years != 0L) measures.add(Measure(years, MeasureUnit.YEAR)) - if (months != 0L) measures.add(Measure(months, MeasureUnit.MONTH)) - if (weeks != 0L) measures.add(Measure(weeks, MeasureUnit.WEEK)) - if (days != 0L) measures.add(Measure(days, MeasureUnit.DAY)) - if (hours != 0L) measures.add(Measure(hours, MeasureUnit.HOUR)) - if (minutes != 0L) measures.add(Measure(minutes, MeasureUnit.MINUTE)) - if (seconds != 0L) measures.add(Measure(seconds, MeasureUnit.SECOND)) - - if (measures.isEmpty()) return null - - @Suppress("SpreadOperator") // There's no other way, really - return fmt.formatMeasures(*measures.toTypedArray()) + // This function is pretty cursed, but then again, Time4J is pretty cursed. + val formatter = Duration.Formatter.ofPattern("#################Y::#M::#D::#h::#m::#s") + val offsetTime = duration.addTo(PlainTimestamp.nowInSystemTime()) + + val newDuration = Duration.`in`( + CalendarUnit.YEARS, + CalendarUnit.MONTHS, + CalendarUnit.DAYS, + ClockUnit.HOURS, + ClockUnit.MINUTES, + ClockUnit.SECONDS + ).between(relativeTo, offsetTime).toTemporalAmount() + + val times = formatter.format(newDuration).split("::") + val (years, months, daysTotal, hours, minutes, seconds) = times.map { it.toLong() } + + val days = daysTotal % DAYS_PER_WEEK + val weeks = daysTotal / DAYS_PER_WEEK + + val fmt = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE) + val measures: MutableList = mutableListOf() + + if (years != 0L) measures.add(Measure(years, MeasureUnit.YEAR)) + if (months != 0L) measures.add(Measure(months, MeasureUnit.MONTH)) + if (weeks != 0L) measures.add(Measure(weeks, MeasureUnit.WEEK)) + if (days != 0L) measures.add(Measure(days, MeasureUnit.DAY)) + if (hours != 0L) measures.add(Measure(hours, MeasureUnit.HOUR)) + if (minutes != 0L) measures.add(Measure(minutes, MeasureUnit.MINUTE)) + if (seconds != 0L) measures.add(Measure(seconds, MeasureUnit.SECOND)) + + if (measures.isEmpty()) return null + + @Suppress("SpreadOperator") // There's no other way, really + return fmt.formatMeasures(*measures.toTypedArray()) } /** @@ -72,8 +72,8 @@ public fun formatT4JDuration( * The string is intended to be readable for humans - "a days, b hours, c minutes, d seconds". */ public fun Duration.toHuman( - locale: Locale, - relativeTo: PlainTimestamp = PlainTimestamp.nowInSystemTime() + locale: Locale, + relativeTo: PlainTimestamp = PlainTimestamp.nowInSystemTime(), ): String? = formatT4JDuration(this, locale, relativeTo) /** @@ -82,8 +82,8 @@ public fun Duration.toHuman( * The string is intended to be readable for humans - "a days, b hours, c minutes, d seconds". */ public suspend fun Duration.toHuman( - context: CommandContext, - relativeTo: PlainTimestamp = PlainTimestamp.nowInSystemTime() + context: CommandContext, + relativeTo: PlainTimestamp = PlainTimestamp.nowInSystemTime(), ): String? = toHuman(context.getLocale(), relativeTo) /** @@ -91,7 +91,7 @@ public suspend fun Duration.toHuman( * you can include in your messages, which Discord should automatically format for users based on their locale. */ public fun Moment.toDiscord(format: TimestampType = TimestampType.Default): String = - format.format(posixTime) + format.format(posixTime) /** * Format the given `PlainTimestamp` to Discord's automatically-formatted timestamp format. This will return a String @@ -101,7 +101,7 @@ public fun Moment.toDiscord(format: TimestampType = TimestampType.Default): Stri * `PlainTimestamp#at(ZonalOffset)` function and call `.toDiscord(format)` with the result.. */ public fun PlainTimestamp.toDiscord(format: TimestampType = TimestampType.Default): String = - atUTC().toDiscord(format) + atUTC().toDiscord(format) /** * Format the given `PlainTimestamp` to Discord's automatically-formatted timestamp format. This will return a String @@ -112,4 +112,4 @@ public fun PlainTimestamp.toDiscord(format: TimestampType = TimestampType.Defaul * result. */ public fun GeneralTimestamp<*>.toDiscord(format: TimestampType = TimestampType.Default): String = - at(ZonalOffset.UTC, StartOfDay.MIDNIGHT).toDiscord(format) + at(ZonalOffset.UTC, StartOfDay.MIDNIGHT).toDiscord(format) diff --git a/modules/time4j/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt b/modules/time4j/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt index 5c652e21d3..07ea77eed8 100644 --- a/modules/time4j/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt +++ b/modules/time4j/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt @@ -14,25 +14,25 @@ import org.koin.core.logger.Level val TEST_SERVER_ID = Snowflake(787452339908116521UL) suspend fun main() { - val bot = ExtensibleBot(env("TOKEN")) { - koinLogLevel = Level.DEBUG - - chatCommands { - defaultPrefix = "?" - - prefix { default -> - if (guildId == TEST_SERVER_ID) { - "!" - } else { - default // "?" - } - } - } - - extensions { - add(::TestExtension) - } - } - - bot.start() + val bot = ExtensibleBot(env("TOKEN")) { + koinLogLevel = Level.DEBUG + + chatCommands { + defaultPrefix = "?" + + prefix { default -> + if (guildId == TEST_SERVER_ID) { + "!" + } else { + default // "?" + } + } + } + + extensions { + add(::TestExtension) + } + } + + bot.start() } diff --git a/modules/time4j/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/TestExtension.kt b/modules/time4j/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/TestExtension.kt index 59c7f660e0..d1b067820a 100644 --- a/modules/time4j/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/TestExtension.kt +++ b/modules/time4j/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/TestExtension.kt @@ -16,25 +16,25 @@ import com.kotlindiscord.kord.extensions.utils.respond // They're IDs @Suppress("UnderscoresInNumericLiterals") class TestExtension : Extension() { - override val name = "test" + override val name = "test" - class TestArgs : Arguments() { - val duration by coalescingT4JDuration { - name = "duration" - description = "Duration argument" - } - } + class TestArgs : Arguments() { + val duration by coalescingT4JDuration { + name = "duration" + description = "Duration argument" + } + } - override suspend fun setup() { - chatCommand(::TestArgs) { - name = "format" - description = "Let's test formatting." + override suspend fun setup() { + chatCommand(::TestArgs) { + name = "format" + description = "Let's test formatting." - action { - message.respond( - arguments.duration.toHuman(this) ?: "Empty duration!" - ) - } - } - } + action { + message.respond( + arguments.duration.toHuman(this) ?: "Empty duration!" + ) + } + } + } } diff --git a/modules/time4j/src/test/resources/logback.groovy b/modules/time4j/src/test/resources/logback.groovy index 76c3dc6373..3c126a7dee 100644 --- a/modules/time4j/src/test/resources/logback.groovy +++ b/modules/time4j/src/test/resources/logback.groovy @@ -11,30 +11,30 @@ def environment = System.getenv().getOrDefault("ENVIRONMENT", "production") def defaultLevel = TRACE if (environment == "spam") { - statusListener(OnConsoleStatusListener) + statusListener(OnConsoleStatusListener) - logger("dev.kord.rest.DefaultGateway", TRACE) + logger("dev.kord.rest.DefaultGateway", TRACE) } else { - // Silence warning about missing native PRNG - logger("io.ktor.util.random", ERROR) + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) } appender("CONSOLE", ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" - withJansi = true - } + withJansi = true + } - target = ConsoleTarget.SystemOut + target = ConsoleTarget.SystemOut } appender("FILE", FileAppender) { - file = "output.log" + file = "output.log" - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } } root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/modules/unsafe/build.gradle.kts b/modules/unsafe/build.gradle.kts index 1b76fd6178..c8dc7c6527 100644 --- a/modules/unsafe/build.gradle.kts +++ b/modules/unsafe/build.gradle.kts @@ -1,28 +1,28 @@ plugins { - `kordex-module` - `published-module` - `ksp-module` + `kordex-module` + `published-module` + `ksp-module` } metadata { - name = "KordEx: Unsafe" - description = "KordEx module that provides extra \"unsafe\" APIs that include lower-level functions and tools" + name = "KordEx: Unsafe" + description = "KordEx module that provides extra \"unsafe\" APIs that include lower-level functions and tools" } dependencies { - implementation(libs.kotlin.stdlib) - implementation(project(":kord-extensions")) + implementation(libs.kotlin.stdlib) + implementation(project(":kord-extensions")) - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - testImplementation(libs.groovy) // For logback config - testImplementation(libs.junit) - testImplementation(libs.logback) + testImplementation(libs.groovy) // For logback config + testImplementation(libs.junit) + testImplementation(libs.logback) - ksp(project(":annotation-processor")) + ksp(project(":annotation-processor")) } dokkaModule { - moduleName.set("Kord Extensions: Unsafe Module") + moduleName.set("Kord Extensions: Unsafe Module") } diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/annotations/UnsafeAPI.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/annotations/UnsafeAPI.kt index 3367fb0828..2d05579567 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/annotations/UnsafeAPI.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/annotations/UnsafeAPI.kt @@ -8,8 +8,8 @@ package com.kotlindiscord.kord.extensions.modules.unsafe.annotations /** Annotation used to mark unsafe APIs. Should be applied to basically everything in this module. **/ @RequiresOptIn( - message = "This API is unsafe, and is only intended for advanced use-cases. If you're not entirely sure that " + - "you need to use this, you should look for a safer API that's provided by a different Kord Extensions module." + message = "This API is unsafe, and is only intended for advanced use-cases. If you're not entirely sure that " + + "you need to use this, you should look for a safer API that's provided by a different Kord Extensions module." ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.TYPEALIAS) diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeCommandEvents.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeCommandEvents.kt index 051cb1b917..a419886d31 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeCommandEvents.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeCommandEvents.kt @@ -19,28 +19,28 @@ import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent /** Event emitted when an unsafe message command is invoked. **/ public data class UnsafeMessageCommandInvocationEvent( - override val command: UnsafeMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent + override val command: UnsafeMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, ) : MessageCommandInvocationEvent> /** Event emitted when an unsafe message command invocation succeeds. **/ public data class UnsafeMessageCommandSucceededEvent( - override val command: UnsafeMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent + override val command: UnsafeMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, ) : MessageCommandSucceededEvent> /** Event emitted when an unsafe message command's checks fail. **/ public data class UnsafeMessageCommandFailedChecksEvent( - override val command: UnsafeMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent, - override val reason: String + override val command: UnsafeMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, + override val reason: String, ) : MessageCommandFailedChecksEvent> /** Event emitted when an unsafe message command invocation fails with an exception. **/ public data class UnsafeMessageCommandFailedWithExceptionEvent( - override val command: UnsafeMessageCommand<*>, - override val event: MessageCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: UnsafeMessageCommand<*>, + override val event: MessageCommandInteractionCreateEvent, + override val throwable: Throwable, ) : MessageCommandFailedWithExceptionEvent> // endregion @@ -49,35 +49,35 @@ public data class UnsafeMessageCommandFailedWithExceptionEvent( /** Event emitted when an unsafe slash command is invoked. **/ public data class UnsafeSlashCommandInvocationEvent( - override val command: UnsafeSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent + override val command: UnsafeSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, ) : SlashCommandInvocationEvent> /** Event emitted when an unsafe slash command invocation succeeds. **/ public data class UnsafeSlashCommandSucceededEvent( - override val command: UnsafeSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent + override val command: UnsafeSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, ) : SlashCommandSucceededEvent> /** Event emitted when an unsafe slash command's checks fail. **/ public data class UnsafeSlashCommandFailedChecksEvent( - override val command: UnsafeSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val reason: String + override val command: UnsafeSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val reason: String, ) : SlashCommandFailedChecksEvent> /** Event emitted when an unsafe slash command's argument parsing fails. **/ public data class UnsafeSlashCommandFailedParsingEvent( - override val command: UnsafeSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val exception: ArgumentParsingException, + override val command: UnsafeSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val exception: ArgumentParsingException, ) : SlashCommandFailedParsingEvent> /** Event emitted when an unsafe slash command invocation fails with an exception. **/ public data class UnsafeSlashCommandFailedWithExceptionEvent( - override val command: UnsafeSlashCommand<*, *>, - override val event: ChatInputCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: UnsafeSlashCommand<*, *>, + override val event: ChatInputCommandInteractionCreateEvent, + override val throwable: Throwable, ) : SlashCommandFailedWithExceptionEvent> // endregion @@ -86,28 +86,28 @@ public data class UnsafeSlashCommandFailedWithExceptionEvent( /** Event emitted when an unsafe user command is invoked. **/ public data class UnsafeUserCommandInvocationEvent( - override val command: UnsafeUserCommand<*>, - override val event: UserCommandInteractionCreateEvent + override val command: UnsafeUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, ) : UserCommandInvocationEvent> /** Event emitted when an unsafe user command invocation succeeds. **/ public data class UnsafeUserCommandSucceededEvent( - override val command: UnsafeUserCommand<*>, - override val event: UserCommandInteractionCreateEvent + override val command: UnsafeUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, ) : UserCommandSucceededEvent> /** Event emitted when an unsafe user command's checks fail. **/ public data class UnsafeUserCommandFailedChecksEvent( - override val command: UnsafeUserCommand<*>, - override val event: UserCommandInteractionCreateEvent, - override val reason: String + override val command: UnsafeUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, + override val reason: String, ) : UserCommandFailedChecksEvent> /** Event emitted when an unsafe user command invocation fails with an exception. **/ public data class UnsafeUserCommandFailedWithExceptionEvent( - override val command: UnsafeUserCommand<*>, - override val event: UserCommandInteractionCreateEvent, - override val throwable: Throwable + override val command: UnsafeUserCommand<*>, + override val event: UserCommandInteractionCreateEvent, + override val throwable: Throwable, ) : UserCommandFailedWithExceptionEvent> // endregion diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeMessageCommand.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeMessageCommand.kt index 026be49629..ab799e8fd1 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeMessageCommand.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeMessageCommand.kt @@ -111,7 +111,7 @@ public class UnsafeMessageCommand( context: UnsafeMessageCommandContext, message: String, failureType: FailureReason<*>, - ) { + ) { when (context.interactionResponse) { is PublicMessageInteractionResponseBehavior -> context.respondPublic { settings.failureResponseBuilder(this, message, failureType) diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeSlashCommand.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeSlashCommand.kt index 6e5df21647..2f977d6399 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeSlashCommand.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeSlashCommand.kt @@ -136,7 +136,7 @@ public class UnsafeSlashCommand( context: UnsafeSlashCommandContext, message: String, failureType: FailureReason<*>, - ) { + ) { when (context.interactionResponse) { is PublicMessageInteractionResponseBehavior -> context.respondPublic { settings.failureResponseBuilder(this, message, failureType) diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeUserCommand.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeUserCommand.kt index a6f71ca59b..98e9540010 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeUserCommand.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/commands/UnsafeUserCommand.kt @@ -112,7 +112,7 @@ public class UnsafeUserCommand( context: UnsafeUserCommandContext, message: String, failureType: FailureReason<*>, - ) { + ) { when (context.interactionResponse) { is PublicMessageInteractionResponseBehavior -> context.respondPublic { settings.failureResponseBuilder(this, message, failureType) diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeMessageCommandContext.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeMessageCommandContext.kt index 5cfbc820ea..d499c08e18 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeMessageCommandContext.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeMessageCommandContext.kt @@ -18,8 +18,8 @@ import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent /** Command context for an unsafe message command. **/ @UnsafeAPI public class UnsafeMessageCommandContext( - override val event: MessageCommandInteractionCreateEvent, - override val command: MessageCommand, M>, - override var interactionResponse: MessageInteractionResponseBehavior?, - cache: MutableStringKeyedMap + override val event: MessageCommandInteractionCreateEvent, + override val command: MessageCommand, M>, + override var interactionResponse: MessageInteractionResponseBehavior?, + cache: MutableStringKeyedMap, ) : MessageCommandContext, M>(event, command, cache), UnsafeInteractionContext diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeSlashCommandContext.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeSlashCommandContext.kt index ed8d8f3ef2..e3a93131e3 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeSlashCommandContext.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeSlashCommandContext.kt @@ -19,8 +19,8 @@ import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent /** Command context for an unsafe slash command. **/ @UnsafeAPI public class UnsafeSlashCommandContext( - override val event: ChatInputCommandInteractionCreateEvent, - override val command: UnsafeSlashCommand, - override var interactionResponse: MessageInteractionResponseBehavior?, - cache: MutableStringKeyedMap + override val event: ChatInputCommandInteractionCreateEvent, + override val command: UnsafeSlashCommand, + override var interactionResponse: MessageInteractionResponseBehavior?, + cache: MutableStringKeyedMap, ) : SlashCommandContext, A, M>(event, command, cache), UnsafeInteractionContext diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeUserCommandContext.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeUserCommandContext.kt index d6c7c7ce6e..d7aa07386c 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeUserCommandContext.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/contexts/UnsafeUserCommandContext.kt @@ -18,8 +18,8 @@ import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent /** Command context for an unsafe user command. **/ @UnsafeAPI public class UnsafeUserCommandContext( - override val event: UserCommandInteractionCreateEvent, - override val command: UserCommand, M>, - override var interactionResponse: MessageInteractionResponseBehavior?, - cache: MutableStringKeyedMap + override val event: UserCommandInteractionCreateEvent, + override val command: UserCommand, M>, + override var interactionResponse: MessageInteractionResponseBehavior?, + cache: MutableStringKeyedMap, ) : UserCommandContext, M>(event, command, cache), UnsafeInteractionContext diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/converters/UnionConverter.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/converters/UnionConverter.kt index e76b820fdd..2dce822060 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/converters/UnionConverter.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/converters/UnionConverter.kt @@ -5,7 +5,7 @@ */ @file:OptIn( - ConverterToOptional::class + ConverterToOptional::class ) @file:Suppress("StringLiteralDuplication") @@ -35,253 +35,253 @@ private typealias GenericConverter = Converter<*, *, *, *> */ @UnsafeAPI public class UnionConverter( - private val converters: Collection, + private val converters: Collection, - typeName: String? = null, - shouldThrow: Boolean = false, + typeName: String? = null, + shouldThrow: Boolean = false, - override val bundle: String? = DEFAULT_KORDEX_BUNDLE, - override var validator: Validator = null + override val bundle: String? = DEFAULT_KORDEX_BUNDLE, + override var validator: Validator = null, ) : CoalescingConverter(shouldThrow) { - private val translations: TranslationsProvider by inject() - - override val signatureTypeString: String = typeName ?: converters.joinToString(" | ") { - translations.translate(it.signatureTypeString, it.bundle) - } - - /** @suppress Internal validation function. **/ - public fun validateUnion() { - val allConverters: MutableList = converters.toMutableList() - - allConverters.removeLast() // The last converter can be any type. - - for (converter in allConverters) { - when (converter) { - is DefaultingConverter<*>, is DefaultingCoalescingConverter<*> -> error( - "Invalid converter: $converter - " + - "Defaulting converters are only supported by union converters if they're the last " + - "provided converter." - ) - - is OptionalConverter<*>, is OptionalCoalescingConverter<*> -> error( - "Invalid converter: $converter - " + - "Optional converters are only supported by union converters if they're the last " + - "provided converter." - ) - } - } - } - - override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { - for (converter in converters) { - @Suppress("TooGenericExceptionCaught") - when (converter) { - is SingleConverter<*> -> try { - val result: Boolean = converter.parse(parser, context, named?.first()) - - if (result) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return 1 - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is DefaultingConverter<*> -> try { - val result: Boolean = converter.parse(parser, context, named?.first()) - - if (result) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return 1 - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is OptionalConverter<*> -> try { - val result: Boolean = converter.parse(parser, context, named?.first()) - - if (result && converter.parsed != null) { - converter.parseSuccess = true - this.parsed = converter.parsed!! - - return 1 - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is ListConverter<*> -> try { - val result: Int = converter.parse(parser, context, named) - - if (result > 0) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return result - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is CoalescingConverter<*> -> try { - val result: Int = converter.parse(parser, context, named) - - if (result > 0) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return result - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is DefaultingCoalescingConverter<*> -> try { - val result: Int = converter.parse(parser, context, named) - - if (result > 0) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return result - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is OptionalCoalescingConverter<*> -> try { - val result: Int = converter.parse(parser, context, named) - - if (result > 0 && converter.parsed != null) { - converter.parseSuccess = true - this.parsed = converter.parsed!! - - return result - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - else -> throw DiscordRelayedException( - context.translate( - "converters.union.error.unknownConverterType", - replacements = arrayOf(converter) - ) - ) - } - } - - return 0 - } - - override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = - StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } - - override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { - for (converter in converters) { - @Suppress("TooGenericExceptionCaught") - when (converter) { - is SingleConverter<*> -> try { - val result: Boolean = converter.parseOption(context, option) - - if (result) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return true - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is DefaultingConverter<*> -> try { - val result: Boolean = converter.parseOption(context, option) - - if (result) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return true - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is OptionalConverter<*> -> try { - val result: Boolean = converter.parseOption(context, option) - - if (result && converter.parsed != null) { - converter.parseSuccess = true - this.parsed = converter.parsed!! - - return true - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is ListConverter<*> -> throw DiscordRelayedException( - context.translate( - "converters.union.error.unknownConverterType", - replacements = arrayOf(converter) - ) - ) - - is CoalescingConverter<*> -> try { - val result: Boolean = converter.parseOption(context, option) - - if (result) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return true - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is DefaultingCoalescingConverter<*> -> try { - val result: Boolean = converter.parseOption(context, option) - - if (result) { - converter.parseSuccess = true - this.parsed = converter.parsed - - return true - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - is OptionalCoalescingConverter<*> -> try { - val result: Boolean = converter.parseOption(context, option) - - if (result && converter.parsed != null) { - converter.parseSuccess = true - this.parsed = converter.parsed!! - - return result - } - } catch (t: Throwable) { - if (shouldThrow) throw t - } - - else -> throw DiscordRelayedException( - context.translate( - "converters.union.error.unknownConverterType", - replacements = arrayOf(converter) - ) - ) - } - } - - return false - } + private val translations: TranslationsProvider by inject() + + override val signatureTypeString: String = typeName ?: converters.joinToString(" | ") { + translations.translate(it.signatureTypeString, it.bundle) + } + + /** @suppress Internal validation function. **/ + public fun validateUnion() { + val allConverters: MutableList = converters.toMutableList() + + allConverters.removeLast() // The last converter can be any type. + + for (converter in allConverters) { + when (converter) { + is DefaultingConverter<*>, is DefaultingCoalescingConverter<*> -> error( + "Invalid converter: $converter - " + + "Defaulting converters are only supported by union converters if they're the last " + + "provided converter." + ) + + is OptionalConverter<*>, is OptionalCoalescingConverter<*> -> error( + "Invalid converter: $converter - " + + "Optional converters are only supported by union converters if they're the last " + + "provided converter." + ) + } + } + } + + override suspend fun parse(parser: StringParser?, context: CommandContext, named: List?): Int { + for (converter in converters) { + @Suppress("TooGenericExceptionCaught") + when (converter) { + is SingleConverter<*> -> try { + val result: Boolean = converter.parse(parser, context, named?.first()) + + if (result) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return 1 + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is DefaultingConverter<*> -> try { + val result: Boolean = converter.parse(parser, context, named?.first()) + + if (result) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return 1 + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is OptionalConverter<*> -> try { + val result: Boolean = converter.parse(parser, context, named?.first()) + + if (result && converter.parsed != null) { + converter.parseSuccess = true + this.parsed = converter.parsed!! + + return 1 + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is ListConverter<*> -> try { + val result: Int = converter.parse(parser, context, named) + + if (result > 0) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return result + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is CoalescingConverter<*> -> try { + val result: Int = converter.parse(parser, context, named) + + if (result > 0) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return result + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is DefaultingCoalescingConverter<*> -> try { + val result: Int = converter.parse(parser, context, named) + + if (result > 0) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return result + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is OptionalCoalescingConverter<*> -> try { + val result: Int = converter.parse(parser, context, named) + + if (result > 0 && converter.parsed != null) { + converter.parseSuccess = true + this.parsed = converter.parsed!! + + return result + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + else -> throw DiscordRelayedException( + context.translate( + "converters.union.error.unknownConverterType", + replacements = arrayOf(converter) + ) + ) + } + } + + return 0 + } + + override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder = + StringChoiceBuilder(arg.displayName, arg.description).apply { required = true } + + override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean { + for (converter in converters) { + @Suppress("TooGenericExceptionCaught") + when (converter) { + is SingleConverter<*> -> try { + val result: Boolean = converter.parseOption(context, option) + + if (result) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return true + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is DefaultingConverter<*> -> try { + val result: Boolean = converter.parseOption(context, option) + + if (result) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return true + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is OptionalConverter<*> -> try { + val result: Boolean = converter.parseOption(context, option) + + if (result && converter.parsed != null) { + converter.parseSuccess = true + this.parsed = converter.parsed!! + + return true + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is ListConverter<*> -> throw DiscordRelayedException( + context.translate( + "converters.union.error.unknownConverterType", + replacements = arrayOf(converter) + ) + ) + + is CoalescingConverter<*> -> try { + val result: Boolean = converter.parseOption(context, option) + + if (result) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return true + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is DefaultingCoalescingConverter<*> -> try { + val result: Boolean = converter.parseOption(context, option) + + if (result) { + converter.parseSuccess = true + this.parsed = converter.parsed + + return true + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + is OptionalCoalescingConverter<*> -> try { + val result: Boolean = converter.parseOption(context, option) + + if (result && converter.parsed != null) { + converter.parseSuccess = true + this.parsed = converter.parsed!! + + return result + } + } catch (t: Throwable) { + if (shouldThrow) throw t + } + + else -> throw DiscordRelayedException( + context.translate( + "converters.union.error.unknownConverterType", + replacements = arrayOf(converter) + ) + ) + } + } + + return false + } } /** @@ -294,27 +294,27 @@ public class UnionConverter( */ @UnsafeAPI public fun Arguments.union( - displayName: String, - description: String, - typeName: String? = null, - shouldThrow: Boolean = false, - vararg converters: GenericConverter, - bundle: String? = null, - validator: Validator = null, + displayName: String, + description: String, + typeName: String? = null, + shouldThrow: Boolean = false, + vararg converters: GenericConverter, + bundle: String? = null, + validator: Validator = null, ): UnionConverter { - val converter: UnionConverter = UnionConverter(converters.toList(), typeName, shouldThrow, bundle, validator) + val converter: UnionConverter = UnionConverter(converters.toList(), typeName, shouldThrow, bundle, validator) - converter.validateUnion() + converter.validateUnion() - this.args.toList().forEach { - if (it.converter in converters) { - this.args.remove(it) - } - } + this.args.toList().forEach { + if (it.converter in converters) { + this.args.remove(it) + } + } - arg(displayName, description, converter) + arg(displayName, description, converter) - return converter + return converter } /** @@ -328,27 +328,27 @@ public fun Arguments.union( */ @UnsafeAPI public fun Arguments.optionalUnion( - displayName: String, - description: String, - typeName: String? = null, - shouldThrow: Boolean = false, - vararg converters: GenericConverter, - bundle: String? = null, - validator: Validator = null + displayName: String, + description: String, + typeName: String? = null, + shouldThrow: Boolean = false, + vararg converters: GenericConverter, + bundle: String? = null, + validator: Validator = null, ): OptionalCoalescingConverter { - val converter: UnionConverter = UnionConverter(converters.toList(), typeName, shouldThrow, bundle) + val converter: UnionConverter = UnionConverter(converters.toList(), typeName, shouldThrow, bundle) - converter.validateUnion() + converter.validateUnion() - this.args.toList().forEach { - if (it.converter in converters) { - this.args.remove(it) - } - } + this.args.toList().forEach { + if (it.converter in converters) { + this.args.remove(it) + } + } - val optionalConverter: OptionalCoalescingConverter = converter.toOptional(nestedValidator = validator) + val optionalConverter: OptionalCoalescingConverter = converter.toOptional(nestedValidator = validator) - arg(displayName, description, optionalConverter) + arg(displayName, description, optionalConverter) - return optionalConverter + return optionalConverter } diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/extensions/_Commands.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/extensions/_Commands.kt index 8e404c6c07..bb7096e3e6 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/extensions/_Commands.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/extensions/_Commands.kt @@ -28,34 +28,34 @@ private val logger = KotlinLogging.logger {} @ExtensionDSL @UnsafeAPI public suspend fun Extension.unsafeMessageCommand( - body: suspend UnsafeMessageCommand.() -> Unit + body: suspend UnsafeMessageCommand.() -> Unit, ): UnsafeMessageCommand { - val commandObj = UnsafeMessageCommand(this) - body(commandObj) + val commandObj = UnsafeMessageCommand(this) + body(commandObj) - return unsafeMessageCommand(commandObj) + return unsafeMessageCommand(commandObj) } /** Register a custom instance of an unsafe message command. **/ @ExtensionDSL @UnsafeAPI public suspend fun Extension.unsafeMessageCommand( - commandObj: UnsafeMessageCommand + commandObj: UnsafeMessageCommand, ): UnsafeMessageCommand { - try { - commandObj.validate() - messageCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } - - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } - - return commandObj + try { + commandObj.validate() + messageCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } + + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } + + return commandObj } // endregion @@ -73,13 +73,13 @@ public suspend fun Extension.unsafeMessageCommand( @ExtensionDSL @UnsafeAPI public suspend fun Extension.unsafeSlashCommand( - arguments: () -> T, - body: suspend UnsafeSlashCommand.() -> Unit + arguments: () -> T, + body: suspend UnsafeSlashCommand.() -> Unit, ): UnsafeSlashCommand { - val commandObj = UnsafeSlashCommand(this, arguments, null, null, null) - body(commandObj) + val commandObj = UnsafeSlashCommand(this, arguments, null, null, null) + body(commandObj) - return unsafeSlashCommand(commandObj) + return unsafeSlashCommand(commandObj) } /** @@ -92,22 +92,22 @@ public suspend fun Extension.unsafeSlashCommand( @ExtensionDSL @UnsafeAPI public suspend fun Extension.unsafeSlashCommand( - commandObj: UnsafeSlashCommand + commandObj: UnsafeSlashCommand, ): UnsafeSlashCommand { - try { - commandObj.validate() - slashCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register subcommand - $e" } - } - - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } - - return commandObj + try { + commandObj.validate() + slashCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register subcommand - $e" } + } + + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } + + return commandObj } /** @@ -120,12 +120,12 @@ public suspend fun Extension.unsafeSlashCommand( @ExtensionDSL @UnsafeAPI public suspend fun Extension.unsafeSlashCommand( - body: suspend UnsafeSlashCommand.() -> Unit + body: suspend UnsafeSlashCommand.() -> Unit, ): UnsafeSlashCommand { - val commandObj = UnsafeSlashCommand(this, null, null, null) - body(commandObj) + val commandObj = UnsafeSlashCommand(this, null, null, null) + body(commandObj) - return unsafeSlashCommand(commandObj) + return unsafeSlashCommand(commandObj) } // endregion @@ -136,33 +136,33 @@ public suspend fun Extension.unsafeSlashCommand( @ExtensionDSL @UnsafeAPI public suspend fun Extension.unsafeUserCommand( - body: suspend UnsafeUserCommand.() -> Unit + body: suspend UnsafeUserCommand.() -> Unit, ): UnsafeUserCommand { - val commandObj = UnsafeUserCommand(this) - body(commandObj) + val commandObj = UnsafeUserCommand(this) + body(commandObj) - return unsafeUserCommand(commandObj) + return unsafeUserCommand(commandObj) } /** Register a custom instance of an unsafe user command. **/ @ExtensionDSL @UnsafeAPI public suspend fun Extension.unsafeUserCommand( - commandObj: UnsafeUserCommand + commandObj: UnsafeUserCommand, ): UnsafeUserCommand { - try { - commandObj.validate() - userCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } - } - - if (applicationCommandRegistry.initialised) { - applicationCommandRegistry.register(commandObj) - } - - return commandObj + try { + commandObj.validate() + userCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register message command ${commandObj.name} - $e" } + } + + if (applicationCommandRegistry.initialised) { + applicationCommandRegistry.register(commandObj) + } + + return commandObj } // endregion diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/extensions/_SlashCommands.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/extensions/_SlashCommands.kt index bce7aee4ea..29313308a2 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/extensions/_SlashCommands.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/extensions/_SlashCommands.kt @@ -31,13 +31,13 @@ private const val SUBCOMMAND_AND_GROUP_LIMIT: Int = 25 */ @UnsafeAPI public suspend fun SlashCommand<*, *, *>.unsafeSubCommand( - arguments: () -> T, - body: suspend UnsafeSlashCommand.() -> Unit + arguments: () -> T, + body: suspend UnsafeSlashCommand.() -> Unit, ): UnsafeSlashCommand { - val commandObj = UnsafeSlashCommand(extension, arguments, null, this, parentGroup) - body(commandObj) + val commandObj = UnsafeSlashCommand(extension, arguments, null, this, parentGroup) + body(commandObj) - return unsafeSubCommand(commandObj) + return unsafeSubCommand(commandObj) } /** @@ -49,26 +49,26 @@ public suspend fun SlashCommand<*, *, *>.unsafeSubCommand( */ @UnsafeAPI public fun SlashCommand<*, *, *>.unsafeSubCommand( - commandObj: UnsafeSlashCommand + commandObj: UnsafeSlashCommand, ): UnsafeSlashCommand { - commandObj.guildId = null - if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { - throw InvalidCommandException( - commandObj.name, - "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." - ) - } - - try { - commandObj.validate() - subCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - kxLogger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - kxLogger.error(e) { "Failed to register subcommand - $e" } - } - - return commandObj + commandObj.guildId = null + if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { + throw InvalidCommandException( + commandObj.name, + "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." + ) + } + + try { + commandObj.validate() + subCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + kxLogger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + kxLogger.error(e) { "Failed to register subcommand - $e" } + } + + return commandObj } /** @@ -80,12 +80,12 @@ public fun SlashCommand<*, *, *>.unsafeSubCommand */ @UnsafeAPI public suspend fun SlashCommand<*, *, *>.unsafeSubCommand( - body: suspend UnsafeSlashCommand.() -> Unit + body: suspend UnsafeSlashCommand.() -> Unit, ): UnsafeSlashCommand { - val commandObj = UnsafeSlashCommand(extension, null, null, this, parentGroup) - body(commandObj) + val commandObj = UnsafeSlashCommand(extension, null, null, this, parentGroup) + body(commandObj) - return unsafeSubCommand(commandObj) + return unsafeSubCommand(commandObj) } // endregion @@ -102,13 +102,13 @@ public suspend fun SlashCommand<*, *, *>.unsafeSubCommand( */ @UnsafeAPI public suspend fun SlashGroup.unsafeSubCommand( - arguments: () -> T, - body: suspend UnsafeSlashCommand.() -> Unit + arguments: () -> T, + body: suspend UnsafeSlashCommand.() -> Unit, ): UnsafeSlashCommand { - val commandObj = UnsafeSlashCommand(parent.extension, arguments, null, parent, this) - body(commandObj) + val commandObj = UnsafeSlashCommand(parent.extension, arguments, null, parent, this) + body(commandObj) - return unsafeSubCommand(commandObj) + return unsafeSubCommand(commandObj) } /** @@ -120,27 +120,27 @@ public suspend fun SlashGroup.unsafeSubCommand( */ @UnsafeAPI public fun SlashGroup.unsafeSubCommand( - commandObj: UnsafeSlashCommand + commandObj: UnsafeSlashCommand, ): UnsafeSlashCommand { commandObj.guildId = null - if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { - throw InvalidCommandException( - commandObj.name, - "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." - ) - } - - try { - commandObj.validate() - subCommands.add(commandObj) - } catch (e: CommandRegistrationException) { - logger.error(e) { "Failed to register subcommand - $e" } - } catch (e: InvalidCommandException) { - logger.error(e) { "Failed to register subcommand - $e" } - } - - return commandObj + if (subCommands.size >= SUBCOMMAND_AND_GROUP_LIMIT) { + throw InvalidCommandException( + commandObj.name, + "Groups may only contain up to $SUBCOMMAND_AND_GROUP_LIMIT commands." + ) + } + + try { + commandObj.validate() + subCommands.add(commandObj) + } catch (e: CommandRegistrationException) { + logger.error(e) { "Failed to register subcommand - $e" } + } catch (e: InvalidCommandException) { + logger.error(e) { "Failed to register subcommand - $e" } + } + + return commandObj } /** @@ -152,12 +152,12 @@ public fun SlashGroup.unsafeSubCommand( */ @UnsafeAPI public suspend fun SlashGroup.unsafeSubCommand( - body: suspend UnsafeSlashCommand.() -> Unit + body: suspend UnsafeSlashCommand.() -> Unit, ): UnsafeSlashCommand { - val commandObj = UnsafeSlashCommand(parent.extension, null, null, parent, this) - body(commandObj) + val commandObj = UnsafeSlashCommand(parent.extension, null, null, parent, this) + body(commandObj) - return unsafeSubCommand(commandObj) + return unsafeSubCommand(commandObj) } // endregion diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialMessageCommandResponse.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialMessageCommandResponse.kt index 797d2fc564..8ab19e9f0a 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialMessageCommandResponse.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialMessageCommandResponse.kt @@ -13,28 +13,28 @@ import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI /** Sealed class representing the initial response types for an unsafe message command. **/ @UnsafeAPI public sealed class InitialMessageCommandResponse { - /** Respond with an ephemeral ack, without any content. **/ - public object EphemeralAck : InitialMessageCommandResponse() + /** Respond with an ephemeral ack, without any content. **/ + public object EphemeralAck : InitialMessageCommandResponse() - /** Respond with a public ack, without any content. **/ - public object PublicAck : InitialMessageCommandResponse() + /** Respond with a public ack, without any content. **/ + public object PublicAck : InitialMessageCommandResponse() - /** Don't respond. Warning: You may not be able to respond in time! **/ - public object None : InitialMessageCommandResponse() + /** Don't respond. Warning: You may not be able to respond in time! **/ + public object None : InitialMessageCommandResponse() - /** - * Respond with an ephemeral ack, including message content. - * - * @param builder Response builder, containing the message content. - */ - public data class EphemeralResponse(val builder: InitialEphemeralMessageResponseBuilder) : - InitialMessageCommandResponse() + /** + * Respond with an ephemeral ack, including message content. + * + * @param builder Response builder, containing the message content. + */ + public data class EphemeralResponse(val builder: InitialEphemeralMessageResponseBuilder) : + InitialMessageCommandResponse() - /** - * Respond with a public ack, including message content. - * - * @param builder Response builder, containing the message content. - **/ - public data class PublicResponse(val builder: InitialPublicMessageResponseBuilder) : - InitialMessageCommandResponse() + /** + * Respond with a public ack, including message content. + * + * @param builder Response builder, containing the message content. + **/ + public data class PublicResponse(val builder: InitialPublicMessageResponseBuilder) : + InitialMessageCommandResponse() } diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialSlashCommandResponse.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialSlashCommandResponse.kt index d2fbbd7a5a..844f0da66a 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialSlashCommandResponse.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialSlashCommandResponse.kt @@ -13,28 +13,28 @@ import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI /** Sealed class representing the initial response types for an unsafe slash command. **/ @UnsafeAPI public sealed class InitialSlashCommandResponse { - /** Respond with an ephemeral ack, without any content. **/ - public object EphemeralAck : InitialSlashCommandResponse() + /** Respond with an ephemeral ack, without any content. **/ + public object EphemeralAck : InitialSlashCommandResponse() - /** Respond with a public ack, without any content. **/ - public object PublicAck : InitialSlashCommandResponse() + /** Respond with a public ack, without any content. **/ + public object PublicAck : InitialSlashCommandResponse() - /** Don't respond. Warning: You may not be able to respond in time! **/ - public object None : InitialSlashCommandResponse() + /** Don't respond. Warning: You may not be able to respond in time! **/ + public object None : InitialSlashCommandResponse() - /** - * Respond with an ephemeral ack, including message content. - * - * @param builder Response builder, containing the message content. - */ - public data class EphemeralResponse(val builder: InitialEphemeralSlashResponseBuilder) : - InitialSlashCommandResponse() + /** + * Respond with an ephemeral ack, including message content. + * + * @param builder Response builder, containing the message content. + */ + public data class EphemeralResponse(val builder: InitialEphemeralSlashResponseBuilder) : + InitialSlashCommandResponse() - /** - * Respond with a public ack, including message content. - * - * @param builder Response builder, containing the message content. - **/ - public data class PublicResponse(val builder: InitialPublicSlashResponseBehavior) : - InitialSlashCommandResponse() + /** + * Respond with a public ack, including message content. + * + * @param builder Response builder, containing the message content. + **/ + public data class PublicResponse(val builder: InitialPublicSlashResponseBehavior) : + InitialSlashCommandResponse() } diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialUserCommandResponse.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialUserCommandResponse.kt index e8ab4b274c..58d237b299 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialUserCommandResponse.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/InitialUserCommandResponse.kt @@ -13,28 +13,28 @@ import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI /** Sealed class representing the initial response types for an unsafe user command. **/ @UnsafeAPI public sealed class InitialUserCommandResponse { - /** Respond with an ephemeral ack, without any content. **/ - public object EphemeralAck : InitialUserCommandResponse() + /** Respond with an ephemeral ack, without any content. **/ + public object EphemeralAck : InitialUserCommandResponse() - /** Respond with a public ack, without any content. **/ - public object PublicAck : InitialUserCommandResponse() + /** Respond with a public ack, without any content. **/ + public object PublicAck : InitialUserCommandResponse() - /** Don't respond. Warning: You may not be able to respond in time! **/ - public object None : InitialUserCommandResponse() + /** Don't respond. Warning: You may not be able to respond in time! **/ + public object None : InitialUserCommandResponse() - /** - * Respond with an ephemeral ack, including message content. - * - * @param builder Response builder, containing the message content. - */ - public data class EphemeralResponse(val builder: InitialEphemeralUserResponseBuilder) : - InitialUserCommandResponse() + /** + * Respond with an ephemeral ack, including message content. + * + * @param builder Response builder, containing the message content. + */ + public data class EphemeralResponse(val builder: InitialEphemeralUserResponseBuilder) : + InitialUserCommandResponse() - /** - * Respond with a public ack, including message content. - * - * @param builder Response builder, containing the message content. - **/ - public data class PublicResponse(val builder: InitialPublicUserResponseBuilder) : - InitialUserCommandResponse() + /** + * Respond with a public ack, including message content. + * + * @param builder Response builder, containing the message content. + **/ + public data class PublicResponse(val builder: InitialPublicUserResponseBuilder) : + InitialUserCommandResponse() } diff --git a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/UnsafeInteractionContext.kt b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/UnsafeInteractionContext.kt index 73a2f358b7..18491b34a6 100644 --- a/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/UnsafeInteractionContext.kt +++ b/modules/unsafe/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/unsafe/types/UnsafeInteractionContext.kt @@ -31,75 +31,75 @@ import java.util.* /** Interface representing a generic, unsafe interaction action context. **/ @UnsafeAPI public interface UnsafeInteractionContext { - /** Response created by acknowledging the interaction. Generic. **/ - public var interactionResponse: MessageInteractionResponseBehavior? + /** Response created by acknowledging the interaction. Generic. **/ + public var interactionResponse: MessageInteractionResponseBehavior? - /** Original interaction event object, for manual acks. **/ - public val event: ApplicationCommandInteractionCreateEvent + /** Original interaction event object, for manual acks. **/ + public val event: ApplicationCommandInteractionCreateEvent } /** Send an ephemeral ack, if the interaction hasn't been acknowledged yet. **/ @UnsafeAPI public suspend fun UnsafeInteractionContext.ackEphemeral( - builder: (suspend InteractionResponseCreateBuilder.() -> Unit)? = null + builder: (suspend InteractionResponseCreateBuilder.() -> Unit)? = null, ): EphemeralMessageInteractionResponseBehavior { - if (interactionResponse != null) { - error("The interaction has already been acknowledged.") - } + if (interactionResponse != null) { + error("The interaction has already been acknowledged.") + } - interactionResponse = if (builder == null) { - event.interaction.deferEphemeralResponseUnsafe() - } else { - event.interaction.respondEphemeral { builder() } - } + interactionResponse = if (builder == null) { + event.interaction.deferEphemeralResponseUnsafe() + } else { + event.interaction.respondEphemeral { builder() } + } - return interactionResponse as EphemeralMessageInteractionResponseBehavior + return interactionResponse as EphemeralMessageInteractionResponseBehavior } /** Send a public ack, if the interaction hasn't been acknowledged yet. **/ @UnsafeAPI public suspend fun UnsafeInteractionContext.ackPublic( - builder: (suspend InteractionResponseCreateBuilder.() -> Unit)? = null + builder: (suspend InteractionResponseCreateBuilder.() -> Unit)? = null, ): PublicMessageInteractionResponseBehavior { - if (interactionResponse != null) { - error("The interaction has already been acknowledged.") - } + if (interactionResponse != null) { + error("The interaction has already been acknowledged.") + } - interactionResponse = if (builder == null) { - event.interaction.deferPublicResponseUnsafe() - } else { - event.interaction.respondPublic { builder() } - } + interactionResponse = if (builder == null) { + event.interaction.deferPublicResponseUnsafe() + } else { + event.interaction.respondPublic { builder() } + } - return interactionResponse as PublicMessageInteractionResponseBehavior + return interactionResponse as PublicMessageInteractionResponseBehavior } /** Respond to the current interaction with an ephemeral followup, or throw if it isn't ephemeral. **/ @UnsafeAPI public suspend inline fun UnsafeInteractionContext.respondEphemeral( - builder: FollowupMessageCreateBuilder.() -> Unit + builder: FollowupMessageCreateBuilder.() -> Unit, ): EphemeralFollowupMessage { - return when (val interaction = interactionResponse) { - is InteractionResponseBehavior -> interaction.createEphemeralFollowup { builder() } + return when (val interaction = interactionResponse) { + is InteractionResponseBehavior -> interaction.createEphemeralFollowup { builder() } - null -> error("Acknowledge the interaction before trying to follow-up.") - else -> error("Unsupported initial interaction response type $interaction - please report this.") - } + null -> error("Acknowledge the interaction before trying to follow-up.") + else -> error("Unsupported initial interaction response type $interaction - please report this.") + } } /** Respond to the current interaction with a public followup. **/ @UnsafeAPI public suspend inline fun UnsafeInteractionContext.respondPublic( - builder: FollowupMessageCreateBuilder.() -> Unit + builder: FollowupMessageCreateBuilder.() -> Unit, ): PublicFollowupMessage { - return when (val interaction = interactionResponse) { - is InteractionResponseBehavior -> interaction.createPublicFollowup { - builder() - } - - null -> error("Acknowledge the interaction before trying to follow-up.") - else -> error("Unsupported initial interaction response type $interaction - please report this.") - } + return when (val interaction = interactionResponse) { + is InteractionResponseBehavior -> interaction.createPublicFollowup { + builder() + } + + null -> error("Acknowledge the interaction before trying to follow-up.") + else -> error("Unsupported initial interaction response type $interaction - please report this.") + } } /** @@ -108,52 +108,52 @@ public suspend inline fun UnsafeInteractionContext.respondPublic( @Suppress("UseIfInsteadOfWhen") @UnsafeAPI public suspend inline fun UnsafeInteractionContext.edit( - builder: InteractionResponseModifyBuilder.() -> Unit + builder: InteractionResponseModifyBuilder.() -> Unit, ): MessageInteractionResponse { - return when (val interaction = interactionResponse) { - is InteractionResponseBehavior -> interaction.edit(builder) + return when (val interaction = interactionResponse) { + is InteractionResponseBehavior -> interaction.edit(builder) - null -> error("Acknowledge the interaction before trying to edit it.") - else -> error("Unsupported initial interaction response type $interaction - please report this.") - } + null -> error("Acknowledge the interaction before trying to edit it.") + else -> error("Unsupported initial interaction response type $interaction - please report this.") + } } /** Create a paginator that edits the original interaction. **/ @UnsafeAPI -public suspend inline fun UnsafeInteractionContext.editingPaginator( - defaultGroup: String = "", - locale: Locale? = null, - builder: (PaginatorBuilder).() -> Unit +public inline fun UnsafeInteractionContext.editingPaginator( + defaultGroup: String = "", + locale: Locale? = null, + builder: (PaginatorBuilder).() -> Unit, ): BaseButtonPaginator { - val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) + val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) - builder(pages) + builder(pages) - return when (val interaction = interactionResponse) { - is PublicMessageInteractionResponseBehavior -> PublicResponsePaginator(pages, interaction) - is EphemeralMessageInteractionResponseBehavior -> EphemeralResponsePaginator(pages, interaction) + return when (val interaction = interactionResponse) { + is PublicMessageInteractionResponseBehavior -> PublicResponsePaginator(pages, interaction) + is EphemeralMessageInteractionResponseBehavior -> EphemeralResponsePaginator(pages, interaction) - null -> error("Acknowledge the interaction before trying to edit it.") - else -> error("Unsupported initial interaction response type - please report this.") - } + null -> error("Acknowledge the interaction before trying to edit it.") + else -> error("Unsupported initial interaction response type - please report this.") + } } /** Create a paginator that creates a follow-up message, and edits that. **/ @Suppress("UseIfInsteadOfWhen") @UnsafeAPI -public suspend inline fun UnsafeInteractionContext.respondingPaginator( - defaultGroup: String = "", - locale: Locale? = null, - builder: (PaginatorBuilder).() -> Unit +public inline fun UnsafeInteractionContext.respondingPaginator( + defaultGroup: String = "", + locale: Locale? = null, + builder: (PaginatorBuilder).() -> Unit, ): BaseButtonPaginator { - val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) + val pages = PaginatorBuilder(locale = locale, defaultGroup = defaultGroup) - builder(pages) + builder(pages) - return when (val interaction = interactionResponse) { - is PublicInteractionResponseBehavior -> PublicFollowUpPaginator(pages, interaction) + return when (val interaction = interactionResponse) { + is PublicInteractionResponseBehavior -> PublicFollowUpPaginator(pages, interaction) - null -> error("Acknowledge the interaction before trying to follow-up.") - else -> error("Initial interaction response was not public.") - } + null -> error("Acknowledge the interaction before trying to follow-up.") + else -> error("Initial interaction response was not public.") + } } diff --git a/plugins/plugin-load-test/src/test/resources/junit-platform.properties b/plugins/plugin-load-test/src/test/resources/junit-platform.properties index 580f511dad..6939a7fb02 100644 --- a/plugins/plugin-load-test/src/test/resources/junit-platform.properties +++ b/plugins/plugin-load-test/src/test/resources/junit-platform.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - junit.jupiter.execution.parallel.enabled=true diff --git a/plugins/plugin-load-test/src/test/resources/logback.groovy b/plugins/plugin-load-test/src/test/resources/logback.groovy index a0c69989f4..22a2ceece2 100644 --- a/plugins/plugin-load-test/src/test/resources/logback.groovy +++ b/plugins/plugin-load-test/src/test/resources/logback.groovy @@ -4,6 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + import ch.qos.logback.classic.encoder.PatternLayoutEncoder import ch.qos.logback.core.joran.spi.ConsoleTarget import ch.qos.logback.core.ConsoleAppender @@ -13,28 +14,28 @@ def environment = System.getenv("ENVIRONMENT") ?: "dev" def defaultLevel = TRACE if (environment == "spam") { - logger("dev.kord.rest.DefaultGateway", TRACE) + logger("dev.kord.rest.DefaultGateway", TRACE) } else { - // Silence warning about missing native PRNG - logger("io.ktor.util.random", ERROR) + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) } appender("CONSOLE", ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" - withJansi = true - } + withJansi = true + } - target = ConsoleTarget.SystemOut + target = ConsoleTarget.SystemOut } appender("FILE", FileAppender) { - file = "output.log" + file = "output.log" - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } } root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/ParentPluginClassLoader.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/ParentPluginClassLoader.kt index 09102517fb..9f2f3a57d8 100644 --- a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/ParentPluginClassLoader.kt +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/ParentPluginClassLoader.kt @@ -22,7 +22,8 @@ public class ParentPluginClassLoader( for (cl in childClassLoaders.values) { try { return cl.loadClassWithoutRecursion(name) - } catch (_: ClassNotFoundException) {} + } catch (_: ClassNotFoundException) { + } } return getSystemClassLoader().loadClass(name) diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginClassLoader.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginClassLoader.kt index 0abd4a1fd9..b2dcfa9164 100644 --- a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginClassLoader.kt +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/PluginClassLoader.kt @@ -5,7 +5,7 @@ import java.net.URLClassLoader public class PluginClassLoader( public val jarPath: String, - private val parent: ParentPluginClassLoader + private val parent: ParentPluginClassLoader, ) : ClassLoader() { private val urlClassLoader = URLClassLoader( arrayOf(URL("file://${parent.pluginsDirectory}/$jarPath")), diff --git a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintChecker.kt b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintChecker.kt index 76ae8996c6..37bcaa9d12 100644 --- a/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintChecker.kt +++ b/plugins/src/main/kotlin/com/kotlindiscord/kord/extensions/plugins/constraints/ConstraintChecker.kt @@ -27,7 +27,7 @@ public open class ConstraintChecker( pluginId: String, manifests: Map, depth: Int = 0, - alreadyExamined: MutableSet = mutableSetOf() + alreadyExamined: MutableSet = mutableSetOf(), ): List { val results: MutableList = mutableListOf() diff --git a/plugins/src/test/resources/junit-platform.properties b/plugins/src/test/resources/junit-platform.properties index 580f511dad..6939a7fb02 100644 --- a/plugins/src/test/resources/junit-platform.properties +++ b/plugins/src/test/resources/junit-platform.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - junit.jupiter.execution.parallel.enabled=true diff --git a/plugins/src/test/resources/logback.groovy b/plugins/src/test/resources/logback.groovy index a0c69989f4..22a2ceece2 100644 --- a/plugins/src/test/resources/logback.groovy +++ b/plugins/src/test/resources/logback.groovy @@ -4,6 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + import ch.qos.logback.classic.encoder.PatternLayoutEncoder import ch.qos.logback.core.joran.spi.ConsoleTarget import ch.qos.logback.core.ConsoleAppender @@ -13,28 +14,28 @@ def environment = System.getenv("ENVIRONMENT") ?: "dev" def defaultLevel = TRACE if (environment == "spam") { - logger("dev.kord.rest.DefaultGateway", TRACE) + logger("dev.kord.rest.DefaultGateway", TRACE) } else { - // Silence warning about missing native PRNG - logger("io.ktor.util.random", ERROR) + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) } appender("CONSOLE", ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" - withJansi = true - } + withJansi = true + } - target = ConsoleTarget.SystemOut + target = ConsoleTarget.SystemOut } appender("FILE", FileAppender) { - file = "output.log" + file = "output.log" - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } } root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/test-bot/build.gradle.kts b/test-bot/build.gradle.kts index 273e5d092d..905741f5c4 100644 --- a/test-bot/build.gradle.kts +++ b/test-bot/build.gradle.kts @@ -1,91 +1,91 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { - repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } - } + repositories { + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + } } plugins { - application + application - `kordex-module` - `ksp-module` + `kordex-module` + `ksp-module` } repositories { - maven { - name = "Sonatype Snapshots" - url = uri("https://oss.sonatype.org/content/repositories/snapshots") - } - - maven { - name = "FabricMC" - url = uri("https://maven.fabricmc.net/") - } - - maven { - name = "QuiltMC (Releases)" - url = uri("https://maven.quiltmc.org/repository/release/") - } - - maven { - name = "QuiltMC (Snapshots)" - url = uri("https://maven.quiltmc.org/repository/snapshot/") - } - - maven { - name = "Shedaniel" - url = uri("https://maven.shedaniel.me") - } - - maven { - name = "JitPack" - url = uri("https://jitpack.io") - } + maven { + name = "Sonatype Snapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + + maven { + name = "FabricMC" + url = uri("https://maven.fabricmc.net/") + } + + maven { + name = "QuiltMC (Releases)" + url = uri("https://maven.quiltmc.org/repository/release/") + } + + maven { + name = "QuiltMC (Snapshots)" + url = uri("https://maven.quiltmc.org/repository/snapshot/") + } + + maven { + name = "Shedaniel" + url = uri("https://maven.shedaniel.me") + } + + maven { + name = "JitPack" + url = uri("https://jitpack.io") + } } dependencies { - implementation(project(":kord-extensions")) + implementation(project(":kord-extensions")) - implementation(project(":extra-modules:extra-mappings")) - implementation(project(":extra-modules:extra-phishing")) - implementation(project(":extra-modules:extra-pluralkit")) + implementation(project(":extra-modules:extra-mappings")) + implementation(project(":extra-modules:extra-phishing")) + implementation(project(":extra-modules:extra-pluralkit")) - implementation(project(":modules:java-time")) - implementation(project(":modules:time4j")) - implementation(project(":modules:unsafe")) + implementation(project(":modules:java-time")) + implementation(project(":modules:time4j")) + implementation(project(":modules:unsafe")) - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - implementation(libs.bundles.commons) - implementation(libs.kotlin.stdlib) + implementation(libs.bundles.commons) + implementation(libs.kotlin.stdlib) - implementation(libs.groovy) // For logback config - implementation(libs.jansi) - implementation(libs.logback) - implementation(libs.logback.groovy) + implementation(libs.groovy) // For logback config + implementation(libs.jansi) + implementation(libs.logback) + implementation(libs.logback.groovy) - ksp(project(":annotation-processor")) - kspTest(project(":annotation-processor")) + ksp(project(":annotation-processor")) + kspTest(project(":annotation-processor")) } val compileKotlin: KotlinCompile by tasks compileKotlin.kotlinOptions { - languageVersion = "1.7" + languageVersion = "1.7" } application { - this.mainClass.set("com.kotlindiscord.kord.extensions.testbot.TestBotKt") + this.mainClass.set("com.kotlindiscord.kord.extensions.testbot.TestBotKt") } detekt { - config.from(files("$projectDir/detekt.yml")) + config.from(files("$projectDir/detekt.yml")) } dokkaModule { diff --git a/test-bot/detekt.yml b/test-bot/detekt.yml index 5a9e871aca..84b82f5bee 100644 --- a/test-bot/detekt.yml +++ b/test-bot/detekt.yml @@ -38,7 +38,7 @@ output-reports: comments: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] AbsentOrWrongFileLicense: active: false licenseTemplateFile: 'license.template' @@ -76,10 +76,10 @@ complexity: ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false ignoreNestingFunctions: false - nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull] + nestingFunctions: [ run, let, apply, with, also, use, forEach, isNotNull, ifNull ] LabeledExpression: active: false - ignoredLabels: [] + ignoredLabels: [ ] LargeClass: active: false threshold: 600 @@ -92,7 +92,7 @@ complexity: constructorThreshold: 7 ignoreDefaultParameters: false ignoreDataClasses: true - ignoreAnnotated: [] + ignoreAnnotated: [ ] MethodOverloading: active: false threshold: 6 @@ -103,14 +103,14 @@ complexity: active: true StringLiteralDuplication: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] thresholdInFiles: 11 thresholdInClasses: 11 thresholdInInterfaces: 11 @@ -168,10 +168,10 @@ exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: true - methodNames: [toString, hashCode, equals, finalize] + methodNames: [ toString, hashCode, equals, finalize ] InstanceOfCheckForException: active: false # It does often make things more readable - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] NotImplementedDeclaration: active: false PrintStackTrace: @@ -203,7 +203,7 @@ exceptions: active: true TooGenericExceptionCaught: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] exceptionNames: - ArrayIndexOutOfBoundsException - Error @@ -348,40 +348,40 @@ naming: active: true ClassNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] classPattern: '[A-Z$][a-zA-Z0-9$]*' ConstructorParameterNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] parameterPattern: '[a-z][A-Za-z0-9]*' privateParameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' ignoreOverridden: true EnumNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] - forbiddenName: [] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] + forbiddenName: [ ] FunctionMaxLength: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] maximumFunctionNameLength: 30 FunctionMinLength: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] minimumFunctionNameLength: 3 FunctionNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' excludeClassPattern: '$^' ignoreOverridden: true FunctionParameterNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' ignoreOverridden: true @@ -398,31 +398,31 @@ naming: active: true ObjectPropertyNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] constantPattern: '[A-Za-z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' PackageNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' TopLevelPropertyNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] constantPattern: '[A-Z][_A-Z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' VariableMaxLength: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] maximumVariableNameLength: 64 VariableMinLength: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] minimumVariableNameLength: 1 VariableNaming: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' @@ -434,10 +434,10 @@ performance: active: true ForEachOnRange: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] SpreadOperator: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] UnnecessaryTemporaryInstantiation: active: true @@ -470,8 +470,8 @@ potential-bugs: active: true LateinitUsage: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] - ignoreAnnotated: [] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] + ignoreAnnotated: [ ] ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: active: true @@ -502,7 +502,7 @@ style: active: true DataClassContainsFunctions: active: true - conversionFunctionPrefix: ['to'] + conversionFunctionPrefix: [ 'to' ] DataClassShouldBeImmutable: active: true EqualsNullCall: @@ -518,15 +518,15 @@ style: includeLineWrapping: false ForbiddenComment: active: false - values: ['TODO:', 'FIXME:', 'STOPSHIP:'] + values: [ 'TODO:', 'FIXME:', 'STOPSHIP:' ] allowedPatterns: '' ForbiddenImport: active: false - imports: [] + imports: [ ] forbiddenPatterns: '' ForbiddenMethodCall: active: false - methods: [] + methods: [ ] ForbiddenVoid: active: true ignoreOverridden: true @@ -534,15 +534,15 @@ style: FunctionOnlyReturningConstant: active: true ignoreOverridableFunction: true - excludedFunctions: ['describeContents'] - ignoreAnnotated: ['dagger.Provides'] + excludedFunctions: [ 'describeContents' ] + ignoreAnnotated: [ 'dagger.Provides' ] LoopWithTooManyJumpStatements: active: true maxJumpCount: 3 MagicNumber: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] - ignoreNumbers: ['-1', '0', '1', '2'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] + ignoreNumbers: [ '-1', '0', '1', '2' ] ignoreHashCodeFunction: true ignorePropertyDeclaration: false ignoreLocalVariableDeclaration: false @@ -589,7 +589,7 @@ style: ReturnCount: active: false max: 2 - excludedFunctions: ['equals'] + excludedFunctions: [ 'equals' ] excludeLabeled: false excludeReturnFromLambda: true excludeGuardClauses: false @@ -609,7 +609,7 @@ style: acceptableLength: 5 UnnecessaryAbstractClass: active: true - ignoreAnnotated: ['dagger.Module'] + ignoreAnnotated: [ 'dagger.Module' ] UnnecessaryAnnotationUseSiteTarget: active: true UnnecessaryApply: @@ -637,7 +637,7 @@ style: active: true UseDataClass: active: true - ignoreAnnotated: [] + ignoreAnnotated: [ ] allowVars: false UseEmptyCounterpart: active: true @@ -655,8 +655,8 @@ style: active: true WildcardImport: active: false - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] - excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*'] + excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] + excludeImports: [ 'java.util.*', 'kotlinx.android.synthetic.*' ] libraries: ForbiddenPublicDataClass: diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/TestBot.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/TestBot.kt index 2b38823221..f0150271c9 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/TestBot.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/TestBot.kt @@ -27,73 +27,73 @@ public val TEST_SERVER_ID: Snowflake = Snowflake(env("TEST_SERVER")) @OptIn(PrivilegedIntent::class) public suspend fun main() { - LogLevel.enabledLevel = LogLevel.fromString(envOrNull("LOG_LEVEL") ?: "INFO") ?: LogLevel.INFO - - val bot = ExtensibleBot(env("TOKEN")) { - koinLogLevel = Level.DEBUG - - chatCommands { - enabled = true - - check { isNotBot() } - } - - applicationCommands { - defaultGuild(TEST_SERVER_ID) - } - - intents { - +Intents.ALL - } - - i18n { - interactionUserLocaleResolver() - - applicationCommandLocale(Locale.CHINESE_CHINA) - applicationCommandLocale(Locale.ENGLISH_GREAT_BRITAIN) - applicationCommandLocale(Locale.ENGLISH_UNITED_STATES) - applicationCommandLocale(Locale.GERMAN) - applicationCommandLocale(Locale.JAPANESE) - } - - members { - all() - } - - extensions { - help { - paginatorTimeout = 30 - } - - extPhishing { - appName = "Integration test bot" - logChannelName = "alerts" - } - - if (envOrNull("PLURALKIT_TESTING") != null) { - extPluralKit() - } - - if (envOrNull("MAPPINGS_TESTING") != null) { - extMappings { } - } - - add(::ArgumentTestExtension) - add(::I18nTestExtension) - add(::ModalTestExtension) - add(::PaginatorTestExtension) - add(::PKTestExtension) - add(::SelectorTestExtension) - add(::NestingTestExtension) - } - - plugins { - pluginPaths.clear() - - pluginPath("test-bot/build/generated/ksp/main/resources") - pluginPath("extra-modules/extra-mappings/build/generated/ksp/main/resources") - } - } - - bot.start() + LogLevel.enabledLevel = LogLevel.fromString(envOrNull("LOG_LEVEL") ?: "INFO") ?: LogLevel.INFO + + val bot = ExtensibleBot(env("TOKEN")) { + koinLogLevel = Level.DEBUG + + chatCommands { + enabled = true + + check { isNotBot() } + } + + applicationCommands { + defaultGuild(TEST_SERVER_ID) + } + + intents { + +Intents.ALL + } + + i18n { + interactionUserLocaleResolver() + + applicationCommandLocale(Locale.CHINESE_CHINA) + applicationCommandLocale(Locale.ENGLISH_GREAT_BRITAIN) + applicationCommandLocale(Locale.ENGLISH_UNITED_STATES) + applicationCommandLocale(Locale.GERMAN) + applicationCommandLocale(Locale.JAPANESE) + } + + members { + all() + } + + extensions { + help { + paginatorTimeout = 30 + } + + extPhishing { + appName = "Integration test bot" + logChannelName = "alerts" + } + + if (envOrNull("PLURALKIT_TESTING") != null) { + extPluralKit() + } + + if (envOrNull("MAPPINGS_TESTING") != null) { + extMappings { } + } + + add(::ArgumentTestExtension) + add(::I18nTestExtension) + add(::ModalTestExtension) + add(::PaginatorTestExtension) + add(::PKTestExtension) + add(::SelectorTestExtension) + add(::NestingTestExtension) + } + + plugins { + pluginPaths.clear() + + pluginPath("test-bot/build/generated/ksp/main/resources") + pluginPath("extra-modules/extra-mappings/build/generated/ksp/main/resources") + } + } + + bot.start() } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/ArgumentTestExtension.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/ArgumentTestExtension.kt index bb4b506b62..81c8c3cda5 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/ArgumentTestExtension.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/ArgumentTestExtension.kt @@ -22,174 +22,174 @@ import dev.kord.core.entity.GuildEmoji import dev.kord.core.entity.channel.Channel public class ArgumentTestExtension : Extension() { - override val name: String = "test-args" + override val name: String = "test-args" - override suspend fun setup() { - publicSlashCommand(::TagArgs) { - name = "test-tag" - description = "Test the tags converter" + override suspend fun setup() { + publicSlashCommand(::TagArgs) { + name = "test-tag" + description = "Test the tags converter" - action { - respond { - content = "Channel provided: `${arguments.channel?.mention}`\n" + + action { + respond { + content = "Channel provided: `${arguments.channel?.mention}`\n" + "Tag provided: `${arguments.tag?.name}`" - } - } - } + } + } + } - publicSlashCommand(::EmojiArguments) { - name = "test-emoji" - description = "Test the emoji converter" + publicSlashCommand(::EmojiArguments) { + name = "test-emoji" + description = "Test the emoji converter" - action { - respond { + action { + respond { val type = if (arguments.emoji is GuildEmoji) { "Guild" } else { "Unicode" } - content = "$type emoji provided: `${arguments.emoji.mention}` (`${arguments.emoji.name}`)" - } - } - } - - publicSlashCommand(::OptionalArgs) { - name = "optional-autocomplete" - description = "Check whether autocomplete works with an optional converter." - - action { - respond { - content = "You provided: `${arguments.response}`" - } - } - } - - publicSlashCommand(::LengthConstrainedArgs) { - name = "length-constrained" - description = "Check if length limits work" - - action { - respond { - content = buildString { - append("You name is: `${arguments.name}`") - arguments.lastName?.let { - append(" `$it`") - } - } - } - } - } - - publicSlashCommand(::AttachmentArguments) { - name = "attachment" - description = "Check attachment command options." - - action { - respond { - content = buildString { - append("You attached: ${arguments.file.filename}.") - - arguments.optionalFile?.let { - append("\nYou also attached: ${it.filename}") - } - } - } - } - } - - publicSlashCommand(::ChannelArguments) { - name = "channel" - description = "Check channel command options." - - action { - respond { - content = buildString { - append("You specified: ${arguments.channel.mention}.") - } - } - } - } - } - - public inner class TagArgs : Arguments() { - override val parseForAutocomplete: Boolean = true - - public val channel: Channel? by optionalChannel { - name = "channel" - description = "Channel to select a tag from" - - requireChannelType(ChannelType.GuildForum) - } - - public val tag: ForumTag? by optionalTag { - name = "tag" - description = "Tag to use" - - channelGetter = { - channel?.asChannelOfOrNull() - } - } - } - - public inner class OptionalArgs : Arguments() { - public val response: String? by optionalString { - name = "response" - description = "Text to receive" - - autoComplete { - suggestStringMap( - mapOf( - "one" to "One", - "two" to "Two", - "three" to "Three" - ) - ) - } - } - } - - public inner class LengthConstrainedArgs : Arguments() { - public val name: String by string { - name = "name" - description = "The user's name." - minLength = 3 - maxLength = 10 - } - - public val lastName: String? by optionalString { - name = "last_name" - description = "The user's last name." - minLength = 4 - maxLength = 15 - } - } - - public inner class AttachmentArguments : Arguments() { - public val file: Attachment by attachment { - name = "file" - description = "An attached file." - } - - public val optionalFile: Attachment? by optionalAttachment { - name = "optional_file" - description = "An optional file." - } - } - - public inner class ChannelArguments : Arguments() { - public val channel: Channel by channel { - name = "channel" - description = "A text channel" - - requireChannelType(ChannelType.GuildText) - } - } - - public inner class EmojiArguments : Arguments() { - public val emoji: Emoji by emoji { - name = "emoji" - description = "A custom or Unicode emoji" - } - } + content = "$type emoji provided: `${arguments.emoji.mention}` (`${arguments.emoji.name}`)" + } + } + } + + publicSlashCommand(::OptionalArgs) { + name = "optional-autocomplete" + description = "Check whether autocomplete works with an optional converter." + + action { + respond { + content = "You provided: `${arguments.response}`" + } + } + } + + publicSlashCommand(::LengthConstrainedArgs) { + name = "length-constrained" + description = "Check if length limits work" + + action { + respond { + content = buildString { + append("You name is: `${arguments.name}`") + arguments.lastName?.let { + append(" `$it`") + } + } + } + } + } + + publicSlashCommand(::AttachmentArguments) { + name = "attachment" + description = "Check attachment command options." + + action { + respond { + content = buildString { + append("You attached: ${arguments.file.filename}.") + + arguments.optionalFile?.let { + append("\nYou also attached: ${it.filename}") + } + } + } + } + } + + publicSlashCommand(::ChannelArguments) { + name = "channel" + description = "Check channel command options." + + action { + respond { + content = buildString { + append("You specified: ${arguments.channel.mention}.") + } + } + } + } + } + + public inner class TagArgs : Arguments() { + override val parseForAutocomplete: Boolean = true + + public val channel: Channel? by optionalChannel { + name = "channel" + description = "Channel to select a tag from" + + requireChannelType(ChannelType.GuildForum) + } + + public val tag: ForumTag? by optionalTag { + name = "tag" + description = "Tag to use" + + channelGetter = { + channel?.asChannelOfOrNull() + } + } + } + + public inner class OptionalArgs : Arguments() { + public val response: String? by optionalString { + name = "response" + description = "Text to receive" + + autoComplete { + suggestStringMap( + mapOf( + "one" to "One", + "two" to "Two", + "three" to "Three" + ) + ) + } + } + } + + public inner class LengthConstrainedArgs : Arguments() { + public val name: String by string { + name = "name" + description = "The user's name." + minLength = 3 + maxLength = 10 + } + + public val lastName: String? by optionalString { + name = "last_name" + description = "The user's last name." + minLength = 4 + maxLength = 15 + } + } + + public inner class AttachmentArguments : Arguments() { + public val file: Attachment by attachment { + name = "file" + description = "An attached file." + } + + public val optionalFile: Attachment? by optionalAttachment { + name = "optional_file" + description = "An optional file." + } + } + + public inner class ChannelArguments : Arguments() { + public val channel: Channel by channel { + name = "channel" + description = "A text channel" + + requireChannelType(ChannelType.GuildText) + } + } + + public inner class EmojiArguments : Arguments() { + public val emoji: Emoji by emoji { + name = "emoji" + description = "A custom or Unicode emoji" + } + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/I18nTestExtension.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/I18nTestExtension.kt index b5f665e23d..43720d6ca2 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/I18nTestExtension.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/I18nTestExtension.kt @@ -23,205 +23,205 @@ import dev.kord.core.behavior.interaction.suggestString import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent public class I18nTestExtension : Extension() { - override val name: String = "test-i18n" - override val bundle: String = "test" - - override suspend fun setup() { - publicSlashCommand { - name = "command.banana-flat" - description = "Translated banana" - - action { - val commandLocale = getLocale() - val interactionLocale = event.interaction.locale?.asJavaLocale() - - assert(commandLocale == interactionLocale) { - "Command locale (`$commandLocale`) does not match interaction locale (`$interactionLocale`)" - } - - respond { content = "Text: ${translate("command.banana")}" } - } - } - - publicSlashCommand { - name = "command.banana-sub" - description = "Translated banana subcommand" - - publicSubCommand { - name = "command.banana" - description = "Translated banana" - - action { - val commandLocale = getLocale() - val interactionLocale = event.interaction.locale?.asJavaLocale() - - assert(commandLocale == interactionLocale) { - "Command locale (`$commandLocale`) does not match interaction locale (`$interactionLocale`)" - } - - respond { content = "Text: ${translate("command.banana")}" } - } - } - } - - publicSlashCommand { - name = "command.banana-group" - description = "Translated banana group" - - group("command.banana") { - description = "Translated banana group" - - publicSubCommand { - name = "command.banana" - description = "Translated banana" - - action { - val commandLocale = getLocale() - val interactionLocale = event.interaction.locale?.asJavaLocale() - - assert(commandLocale == interactionLocale) { - "Command locale (`$commandLocale`) does not match interaction locale (`$interactionLocale`)" - } - - respond { content = "Text: ${translate("command.banana")}" } - } - } - } - } - - publicSlashCommand(::I18nTestArguments) { - name = "command.fruit" - description = "command.fruit" - - action { - respond { - content = translate("command.fruit.response", arrayOf(arguments.fruit)) - } - } - } - - publicSlashCommand(::I18nTestNamedArguments) { - name = "command.apple" - description = "command.apple" - - action { - respond { - content = translate( - "command.apple.response", - - mapOf( - "name" to arguments.name, - "appleCount" to arguments.count - ) - ) - } - } - } - - ephemeralSlashCommand { - name = "test-translated-checks" - description = "Command that always fails, to check CheckContext translations." - - check { - translatedChecks() - } - - action { - // This command is expected to always fail, in order to test checks. - respond { - content = "It is impossible to get here." - } - } - } - - ephemeralSlashCommand(::I18nTestValidations) { - name = "test-translated-validations" - description = "Command with arguments that always fail validations." - - action { - // This command is expected to always fail, in order to test argument validations. - respond { - content = "It is impossible to get here." - } - } - } - } - - private suspend fun CheckContextWithCache.translatedChecks() { - val user = userFor(event) - this.defaultBundle = bundle - - if (user == null) { - fail("Could not get user.") - return - } - - fail( - buildList { - // Translate, with default bundle - add(translate("check.simple")) - // Translate with a different bundle - add(translate("check.simple", "custom")) - // Translate with default bundle, and positional parameters - add(translate("check.positionalParameters", arrayOf(user.mention, user.id))) - // Translate with a different bundle, and positional parameters - add(translate("check.positionalParameters", "custom", arrayOf(user.mention, user.id))) - // Translate with default bundle, named parameters - add(translate("check.namedParameters", replacements = mapOf("user" to user.mention, "id" to user.id))) - // Translate with a different bundle, and named parameters - add(translate("check.namedParameters", "custom", mapOf("user" to user.mention, "id" to user.id))) - }.joinToString("\n") - ) - } - - private inner class I18nTestValidations : Arguments() { - val name by string { - name = "name" - description = "Will always fail to validate." - validate { - defaultBundle = bundle - fail( - buildList { - // Translate, with default bundle - add(translate("validation.simple")) - // Translate with a different bundle - add(translate("validation.simple", "custom")) - // Translate with default bundle, and positional parameters - add(translate("validation.positionalParameters", arrayOf(value))) - // Translate with a different bundle, and positional parameters - add(translate("validation.positionalParameters", "custom", arrayOf(value))) - // Translate with default bundle, named parameters - add(translate("validation.namedParameters", mapOf("value" to value))) - // Translate with a different bundle, and named parameters - add(translate("validation.namedParameters", "custom", mapOf("value" to value))) - }.joinToString("\n") - ) - } - } - } + override val name: String = "test-i18n" + override val bundle: String = "test" + + override suspend fun setup() { + publicSlashCommand { + name = "command.banana-flat" + description = "Translated banana" + + action { + val commandLocale = getLocale() + val interactionLocale = event.interaction.locale?.asJavaLocale() + + assert(commandLocale == interactionLocale) { + "Command locale (`$commandLocale`) does not match interaction locale (`$interactionLocale`)" + } + + respond { content = "Text: ${translate("command.banana")}" } + } + } + + publicSlashCommand { + name = "command.banana-sub" + description = "Translated banana subcommand" + + publicSubCommand { + name = "command.banana" + description = "Translated banana" + + action { + val commandLocale = getLocale() + val interactionLocale = event.interaction.locale?.asJavaLocale() + + assert(commandLocale == interactionLocale) { + "Command locale (`$commandLocale`) does not match interaction locale (`$interactionLocale`)" + } + + respond { content = "Text: ${translate("command.banana")}" } + } + } + } + + publicSlashCommand { + name = "command.banana-group" + description = "Translated banana group" + + group("command.banana") { + description = "Translated banana group" + + publicSubCommand { + name = "command.banana" + description = "Translated banana" + + action { + val commandLocale = getLocale() + val interactionLocale = event.interaction.locale?.asJavaLocale() + + assert(commandLocale == interactionLocale) { + "Command locale (`$commandLocale`) does not match interaction locale (`$interactionLocale`)" + } + + respond { content = "Text: ${translate("command.banana")}" } + } + } + } + } + + publicSlashCommand(::I18nTestArguments) { + name = "command.fruit" + description = "command.fruit" + + action { + respond { + content = translate("command.fruit.response", arrayOf(arguments.fruit)) + } + } + } + + publicSlashCommand(::I18nTestNamedArguments) { + name = "command.apple" + description = "command.apple" + + action { + respond { + content = translate( + "command.apple.response", + + mapOf( + "name" to arguments.name, + "appleCount" to arguments.count + ) + ) + } + } + } + + ephemeralSlashCommand { + name = "test-translated-checks" + description = "Command that always fails, to check CheckContext translations." + + check { + translatedChecks() + } + + action { + // This command is expected to always fail, in order to test checks. + respond { + content = "It is impossible to get here." + } + } + } + + ephemeralSlashCommand(::I18nTestValidations) { + name = "test-translated-validations" + description = "Command with arguments that always fail validations." + + action { + // This command is expected to always fail, in order to test argument validations. + respond { + content = "It is impossible to get here." + } + } + } + } + + private suspend fun CheckContextWithCache.translatedChecks() { + val user = userFor(event) + this.defaultBundle = bundle + + if (user == null) { + fail("Could not get user.") + return + } + + fail( + buildList { + // Translate, with default bundle + add(translate("check.simple")) + // Translate with a different bundle + add(translate("check.simple", "custom")) + // Translate with default bundle, and positional parameters + add(translate("check.positionalParameters", arrayOf(user.mention, user.id))) + // Translate with a different bundle, and positional parameters + add(translate("check.positionalParameters", "custom", arrayOf(user.mention, user.id))) + // Translate with default bundle, named parameters + add(translate("check.namedParameters", replacements = mapOf("user" to user.mention, "id" to user.id))) + // Translate with a different bundle, and named parameters + add(translate("check.namedParameters", "custom", mapOf("user" to user.mention, "id" to user.id))) + }.joinToString("\n") + ) + } + + private inner class I18nTestValidations : Arguments() { + val name by string { + name = "name" + description = "Will always fail to validate." + validate { + defaultBundle = bundle + fail( + buildList { + // Translate, with default bundle + add(translate("validation.simple")) + // Translate with a different bundle + add(translate("validation.simple", "custom")) + // Translate with default bundle, and positional parameters + add(translate("validation.positionalParameters", arrayOf(value))) + // Translate with a different bundle, and positional parameters + add(translate("validation.positionalParameters", "custom", arrayOf(value))) + // Translate with default bundle, named parameters + add(translate("validation.namedParameters", mapOf("value" to value))) + // Translate with a different bundle, and named parameters + add(translate("validation.namedParameters", "custom", mapOf("value" to value))) + }.joinToString("\n") + ) + } + } + } } internal class I18nTestArguments : Arguments() { - val fruit by string { - name = "command.fruit" - description = "command.fruit" - - autoComplete { - suggestString { - listOf("Banana", "Apple", "Cherry").forEach { choice(it, it) } - } - } - } + val fruit by string { + name = "command.fruit" + description = "command.fruit" + + autoComplete { + suggestString { + listOf("Banana", "Apple", "Cherry").forEach { choice(it, it) } + } + } + } } internal class I18nTestNamedArguments : Arguments() { - val name by string { - name = "command.apple.argument.name" - description = "command.apple.argument.name" - } - - val count by int { - name = "command.apple.argument.count" - description = "command.apple.argument.count" - } + val name by string { + name = "command.apple.argument.name" + description = "command.apple.argument.name" + } + + val count by int { + name = "command.apple.argument.count" + description = "command.apple.argument.count" + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/ModalTestExtension.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/ModalTestExtension.kt index da061f8f30..72d19b0662 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/ModalTestExtension.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/ModalTestExtension.kt @@ -20,157 +20,157 @@ import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand import com.kotlindiscord.kord.extensions.extensions.publicUserCommand public class ModalTestExtension : Extension() { - override val name: String = "modals" - override val bundle: String = "test.strings" - - @Suppress("StringLiteralDuplication") - override suspend fun setup() { - publicUserCommand(::Modal) { - name = "Modal" - - action { modal -> - respond { - content = buildString { - if (modal == null) { - append("**No modal found!**") - - return@buildString - } - - append("**Line:** `") - appendLine(modal.line.value) - append("`") - appendLine() - - appendLine("**Paragraph:** ```") - appendLine(modal.paragraph.value) - append("```") - appendLine() - } - } - } - } - - publicMessageCommand(::Modal) { - name = "Modal" - - action { modal -> - respond { - content = buildString { - if (modal == null) { - append("**No modal found!**") - - return@buildString - } - - append("**Line:** `") - appendLine(modal.line.value) - append("`") - appendLine() - - appendLine("**Paragraph:** ```") - appendLine(modal.paragraph.value) - append("```") - appendLine() - } - } - } - } - - publicSlashCommand { - name = "modals" - description = "Modal testing commands" - - publicSubCommand { - name = "button" - description = "Test a modal response to a button" - - action { - respond { - components { - publicButton(::Modal) { - bundle = "test.strings" - label = "Modal!" - - action { modal -> - respond { - content = buildString { - if (modal == null) { - append("**No modal found!**") - - return@buildString - } - - append("**Line:** `") - appendLine(modal.line.value) - append("`") - appendLine() - - appendLine("**Paragraph:** ```") - appendLine(modal.paragraph.value) - append("```") - appendLine() - } - } - } - } - } - } - } - } - - publicSubCommand(::Args, ::Modal) { - name = "command" - description = "Test a modal response to a command" - - action { modal -> - respond { - content = buildString { - append("**Argument:** `") - appendLine(arguments.str) - append("`") - appendLine() - - if (modal == null) { - append("**No modal found!**") - - return@buildString - } - - append("**Line:** `") - appendLine(modal.line.value) - append("`") - appendLine() - - appendLine("**Paragraph:** ```") - appendLine(modal.paragraph.value) - append("```") - appendLine() - } - } - } - } - } - } - - public inner class Args : Arguments() { - public val str: String by string { - name = "string" - description = "A string argument" - } - } - - public inner class Modal : ModalForm() { - override var title: String = "modal.title" - - public val line: LineTextWidget = lineText { - label = "modal.line" - placeholder = "modal.line.placeholder" - } - - public val paragraph: ParagraphTextWidget = paragraphText { - label = "modal.paragraph" - placeholder = "modal.paragraph.placeholder" - } - } + override val name: String = "modals" + override val bundle: String = "test.strings" + + @Suppress("StringLiteralDuplication") + override suspend fun setup() { + publicUserCommand(::Modal) { + name = "Modal" + + action { modal -> + respond { + content = buildString { + if (modal == null) { + append("**No modal found!**") + + return@buildString + } + + append("**Line:** `") + appendLine(modal.line.value) + append("`") + appendLine() + + appendLine("**Paragraph:** ```") + appendLine(modal.paragraph.value) + append("```") + appendLine() + } + } + } + } + + publicMessageCommand(::Modal) { + name = "Modal" + + action { modal -> + respond { + content = buildString { + if (modal == null) { + append("**No modal found!**") + + return@buildString + } + + append("**Line:** `") + appendLine(modal.line.value) + append("`") + appendLine() + + appendLine("**Paragraph:** ```") + appendLine(modal.paragraph.value) + append("```") + appendLine() + } + } + } + } + + publicSlashCommand { + name = "modals" + description = "Modal testing commands" + + publicSubCommand { + name = "button" + description = "Test a modal response to a button" + + action { + respond { + components { + publicButton(::Modal) { + bundle = "test.strings" + label = "Modal!" + + action { modal -> + respond { + content = buildString { + if (modal == null) { + append("**No modal found!**") + + return@buildString + } + + append("**Line:** `") + appendLine(modal.line.value) + append("`") + appendLine() + + appendLine("**Paragraph:** ```") + appendLine(modal.paragraph.value) + append("```") + appendLine() + } + } + } + } + } + } + } + } + + publicSubCommand(::Args, ::Modal) { + name = "command" + description = "Test a modal response to a command" + + action { modal -> + respond { + content = buildString { + append("**Argument:** `") + appendLine(arguments.str) + append("`") + appendLine() + + if (modal == null) { + append("**No modal found!**") + + return@buildString + } + + append("**Line:** `") + appendLine(modal.line.value) + append("`") + appendLine() + + appendLine("**Paragraph:** ```") + appendLine(modal.paragraph.value) + append("```") + appendLine() + } + } + } + } + } + } + + public inner class Args : Arguments() { + public val str: String by string { + name = "string" + description = "A string argument" + } + } + + public inner class Modal : ModalForm() { + override var title: String = "modal.title" + + public val line: LineTextWidget = lineText { + label = "modal.line" + placeholder = "modal.line.placeholder" + } + + public val paragraph: ParagraphTextWidget = paragraphText { + label = "modal.paragraph" + placeholder = "modal.paragraph.placeholder" + } + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/NestingTestExtension.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/NestingTestExtension.kt index e0d5efaf5c..e4f82630c6 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/NestingTestExtension.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/NestingTestExtension.kt @@ -12,38 +12,38 @@ import com.kotlindiscord.kord.extensions.extensions.Extension import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand public class NestingTestExtension : Extension() { - override val name: String = "test-nesting" - - override suspend fun setup() { - publicSlashCommand { - name = "root" - description = "Insert thoughtful proverb here" - - group("group") { - description = "Insert required description here" - - publicSubCommand { - name = "subcommand" - description = "...in a group!" - - action { - respond { - content = "Ah-ah-ah!" - } - } - } - } - - publicSubCommand { - name = "subcommand" - description = "...NEXT to a group!" - - action { - respond { - content = "Ah-ah-ah!" - } - } - } - } - } + override val name: String = "test-nesting" + + override suspend fun setup() { + publicSlashCommand { + name = "root" + description = "Insert thoughtful proverb here" + + group("group") { + description = "Insert required description here" + + publicSubCommand { + name = "subcommand" + description = "...in a group!" + + action { + respond { + content = "Ah-ah-ah!" + } + } + } + } + + publicSubCommand { + name = "subcommand" + description = "...NEXT to a group!" + + action { + respond { + content = "Ah-ah-ah!" + } + } + } + } + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/PKTestExtension.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/PKTestExtension.kt index 55f148b205..cd079ce372 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/PKTestExtension.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/PKTestExtension.kt @@ -13,63 +13,63 @@ import com.kotlindiscord.kord.extensions.utils.respond import dev.kord.core.behavior.channel.createMessage public class PKTestExtension : Extension() { - override val name: String = "test-pluralkit" + override val name: String = "test-pluralkit" - override suspend fun setup() { - event { - action { - event.message.respond(pingInReply = false) { - content = "Proxied PK message created: `${event.message.id}`" - } - } - } + override suspend fun setup() { + event { + action { + event.message.respond(pingInReply = false) { + content = "Proxied PK message created: `${event.message.id}`" + } + } + } - event { - action { - event.getMessage().respond(pingInReply = false) { - content = "Proxied PK message updated: `${event.message.id}`" - } - } - } + event { + action { + event.getMessage().respond(pingInReply = false) { + content = "Proxied PK message updated: `${event.message.id}`" + } + } + } - event { - action { - event.channel.createMessage { - content = "Proxied PK message deleted: `${event.message?.id}`" - } - } - } + event { + action { + event.channel.createMessage { + content = "Proxied PK message deleted: `${event.message?.id}`" + } + } + } - event { - check { failIf(event.message.getAuthorAsMemberOrNull()?.isBot != false) } + event { + check { failIf(event.message.getAuthorAsMemberOrNull()?.isBot != false) } - action { - action { - event.message.respond(pingInReply = false) { - content = "Non-proxied message created: `${event.message.id}`" - } - } - } - } + action { + action { + event.message.respond(pingInReply = false) { + content = "Non-proxied message created: `${event.message.id}`" + } + } + } + } - event { - check { failIf(event.message.asMessageOrNull()?.getAuthorAsMemberOrNull()?.isBot != false) } + event { + check { failIf(event.message.asMessageOrNull()?.getAuthorAsMemberOrNull()?.isBot != false) } - action { - event.getMessage().respond(pingInReply = false) { - content = "Non-proxied message updated: `${event.message.id}`" - } - } - } + action { + event.getMessage().respond(pingInReply = false) { + content = "Non-proxied message updated: `${event.message.id}`" + } + } + } - event { - check { failIf(event.message?.getAuthorAsMemberOrNull()?.isBot != false) } + event { + check { failIf(event.message?.getAuthorAsMemberOrNull()?.isBot != false) } - action { - event.channel.createMessage { - content = "Non-proxied message deleted: `${event.message?.id}`" - } - } - } - } + action { + event.channel.createMessage { + content = "Non-proxied message deleted: `${event.message?.id}`" + } + } + } + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/PaginatorTestExtension.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/PaginatorTestExtension.kt index f6bb8ccf92..36ddfe7703 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/PaginatorTestExtension.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/PaginatorTestExtension.kt @@ -13,143 +13,143 @@ import com.kotlindiscord.kord.extensions.extensions.Extension import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand public class PaginatorTestExtension : Extension() { - override val name: String = "test-paginator" - - override suspend fun setup() { - publicSlashCommand { - name = "paginator" - description = "Paginator testing commands." - - publicSubCommand { - name = "default" - description = "Test a default-grouped paginator with pages." - - action { - editingPaginator { - page { - description = "Page one!" - } - - page { - description = "Page two!" - } - - page { - description = "Page three!" - } - }.send() - } - } - - publicSubCommand { - name = "chunked" - description = "Test a chunked default-group paginator with pages." - - action { - editingPaginator { + override val name: String = "test-paginator" + + override suspend fun setup() { + publicSlashCommand { + name = "paginator" + description = "Paginator testing commands." + + publicSubCommand { + name = "default" + description = "Test a default-grouped paginator with pages." + + action { + editingPaginator { + page { + description = "Page one!" + } + + page { + description = "Page two!" + } + + page { + description = "Page three!" + } + }.send() + } + } + + publicSubCommand { + name = "chunked" + description = "Test a chunked default-group paginator with pages." + + action { + editingPaginator { chunkedPages = 3 - page { - description = "Page one!" - } + page { + description = "Page one!" + } - page { - description = "Page one!" - } + page { + description = "Page one!" + } - page { - description = "Page one!" - } + page { + description = "Page one!" + } - page { - description = "Page two!" - } + page { + description = "Page two!" + } - page { - description = "Page two!" - } + page { + description = "Page two!" + } - page { - description = "Page two!" - } + page { + description = "Page two!" + } - page { - description = "Page three (with only 2 chunks)" - } + page { + description = "Page three (with only 2 chunks)" + } - page { - description = "Page three (with only 2 chunks)" - } - }.send() - } - } + page { + description = "Page three (with only 2 chunks)" + } + }.send() + } + } - publicSubCommand { - name = "chunked-small" - description = "Test a chunked default-group paginator with one page." + publicSubCommand { + name = "chunked-small" + description = "Test a chunked default-group paginator with one page." - action { - editingPaginator { + action { + editingPaginator { chunkedPages = 2 - page { + page { title = "Page one!" - description = "Page one!" - } - }.send() - } - } - - publicSubCommand { - name = "custom-one" - description = "Test a custom-grouped paginator with pages, approach 1." - - action { - editingPaginator("custom") { - page(group = "custom") { - description = "Page one!" - } - - page(group = "custom") { - description = "Page two!" - } - - page(group = "custom") { - description = "Page three!" - } - }.send() - } - } - - publicSubCommand { - name = "custom-two" - description = "Test a custom-grouped paginator with pages, approach 2." - - action { - editingPaginator("custom") { - page("custom") { - description = "Page one!" - } - - page("custom") { - description = "Page two!" - } - - page("custom") { - description = "Page three!" - } - }.send() - } - } - - publicSubCommand { - name = "custom-pageless" - description = "Test a custom-grouped paginator without pages." - - action { - editingPaginator("custom") { }.send() - } - } - } - } + description = "Page one!" + } + }.send() + } + } + + publicSubCommand { + name = "custom-one" + description = "Test a custom-grouped paginator with pages, approach 1." + + action { + editingPaginator("custom") { + page(group = "custom") { + description = "Page one!" + } + + page(group = "custom") { + description = "Page two!" + } + + page(group = "custom") { + description = "Page three!" + } + }.send() + } + } + + publicSubCommand { + name = "custom-two" + description = "Test a custom-grouped paginator with pages, approach 2." + + action { + editingPaginator("custom") { + page("custom") { + description = "Page one!" + } + + page("custom") { + description = "Page two!" + } + + page("custom") { + description = "Page three!" + } + }.send() + } + } + + publicSubCommand { + name = "custom-pageless" + description = "Test a custom-grouped paginator without pages." + + action { + editingPaginator("custom") { }.send() + } + } + } + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/SelectorTestExtension.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/SelectorTestExtension.kt index 3720769b3b..9fda52b404 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/SelectorTestExtension.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/extensions/SelectorTestExtension.kt @@ -16,136 +16,136 @@ import dev.kord.core.behavior.channel.asChannelOf import dev.kord.core.entity.channel.TextChannel public class SelectorTestExtension : Extension() { - override val name: String = "Select Menus Test" - - override suspend fun setup() { - publicSlashCommand { - name = "selector" - description = "Test selectors." - - publicSubCommand { - name = "public" - description = "Test public selectors." - - action { - respond { - components { - publicStringSelectMenu { - option("hi", "1") - option("hi hi", "2") - maximumChoices = null - - action { - respond { content = selected.joinToString("\n") } - } - } - - publicUserSelectMenu { - maximumChoices = null - - action { - respond { - content = selected.map { it.asUser().username }.joinToString("\n") - } - } - } - - publicRoleSelectMenu { - maximumChoices = null - - action { - respond { - content = selected.map { it.asRole().name }.joinToString("\n") - } - } - } - - publicChannelSelectMenu { - maximumChoices = null - - action { - respond { - content = selected.map { it.id.value }.joinToString("\n") - } - } - } - - publicChannelSelectMenu { - maximumChoices = null - channelType(ChannelType.GuildText) - - action { - respond { - content = selected.map { it.asChannelOf().name }.joinToString("\n") - } - } - } - } - } - } - } - - ephemeralSubCommand { - name = "ephemeral" - description = "Test ephemeral selectors." - - action { - respond { - components { - ephemeralStringSelectMenu { - option("hi", "1") - option("hi hi", "2") - maximumChoices = null - - action { - respond { content = selected.joinToString("\n") } - } - } - - ephemeralUserSelectMenu { - maximumChoices = null - - action { - respond { - content = selected.map { it.asUser().username }.joinToString("\n") - } - } - } - - ephemeralRoleSelectMenu { - maximumChoices = null - - action { - respond { - content = selected.map { it.asRole().name }.joinToString("\n") - } - } - } - - ephemeralChannelSelectMenu { - maximumChoices = null - - action { - respond { - content = selected.map { it.id.value }.joinToString("\n") - } - } - } - - ephemeralChannelSelectMenu { - maximumChoices = null - channelType(ChannelType.GuildText) - - action { - respond { - content = selected.map { it.asChannelOf().name }.joinToString("\n") - } - } - } - } - } - } - } - } - } + override val name: String = "Select Menus Test" + + override suspend fun setup() { + publicSlashCommand { + name = "selector" + description = "Test selectors." + + publicSubCommand { + name = "public" + description = "Test public selectors." + + action { + respond { + components { + publicStringSelectMenu { + option("hi", "1") + option("hi hi", "2") + maximumChoices = null + + action { + respond { content = selected.joinToString("\n") } + } + } + + publicUserSelectMenu { + maximumChoices = null + + action { + respond { + content = selected.map { it.asUser().username }.joinToString("\n") + } + } + } + + publicRoleSelectMenu { + maximumChoices = null + + action { + respond { + content = selected.map { it.asRole().name }.joinToString("\n") + } + } + } + + publicChannelSelectMenu { + maximumChoices = null + + action { + respond { + content = selected.map { it.id.value }.joinToString("\n") + } + } + } + + publicChannelSelectMenu { + maximumChoices = null + channelType(ChannelType.GuildText) + + action { + respond { + content = selected.map { it.asChannelOf().name }.joinToString("\n") + } + } + } + } + } + } + } + + ephemeralSubCommand { + name = "ephemeral" + description = "Test ephemeral selectors." + + action { + respond { + components { + ephemeralStringSelectMenu { + option("hi", "1") + option("hi hi", "2") + maximumChoices = null + + action { + respond { content = selected.joinToString("\n") } + } + } + + ephemeralUserSelectMenu { + maximumChoices = null + + action { + respond { + content = selected.map { it.asUser().username }.joinToString("\n") + } + } + } + + ephemeralRoleSelectMenu { + maximumChoices = null + + action { + respond { + content = selected.map { it.asRole().name }.joinToString("\n") + } + } + } + + ephemeralChannelSelectMenu { + maximumChoices = null + + action { + respond { + content = selected.map { it.id.value }.joinToString("\n") + } + } + } + + ephemeralChannelSelectMenu { + maximumChoices = null + channelType(ChannelType.GuildText) + + action { + respond { + content = selected.map { it.asChannelOf().name }.joinToString("\n") + } + } + } + } + } + } + } + } + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPlugin.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPlugin.kt index c17161c833..efb0bddb62 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPlugin.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPlugin.kt @@ -13,25 +13,25 @@ import com.kotlindiscord.kord.extensions.storage.StorageUnit import org.pf4j.PluginWrapper @WiredPlugin( - TestPlugin.PLUGIN_ID, - "0.0.1", - "kord-extensions", - "KordEx testing plugin", - "MPL 2.0" + TestPlugin.PLUGIN_ID, + "0.0.1", + "kord-extensions", + "KordEx testing plugin", + "MPL 2.0" ) public class TestPlugin(wrapper: PluginWrapper) : KordExPlugin(wrapper) { - override suspend fun setup() { - extension(::TestPluginExtension) - } + override suspend fun setup() { + extension(::TestPluginExtension) + } - public companion object { - public const val PLUGIN_ID: String = "test-plugin" + public companion object { + public const val PLUGIN_ID: String = "test-plugin" - internal val DATA_UNIT = StorageUnit( - StorageType.Data, - PLUGIN_ID, - "test", - TestPluginData::class - ) - } + internal val DATA_UNIT = StorageUnit( + StorageType.Data, + PLUGIN_ID, + "test", + TestPluginData::class + ) + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPluginData.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPluginData.kt index 76dfb7ecdb..8b019c157f 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPluginData.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPluginData.kt @@ -13,6 +13,6 @@ import net.peanuuutz.tomlkt.TomlComment @Serializable @Suppress("DataClassShouldBeImmutable") // No. public data class TestPluginData( - @TomlComment("A test value. Nothing special here.") - var key: String + @TomlComment("A test value. Nothing special here.") + var key: String, ) : Data diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPluginExtension.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPluginExtension.kt index 02d5c71427..c1c28ed708 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPluginExtension.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/plugin/TestPluginExtension.kt @@ -18,150 +18,150 @@ import org.koin.core.component.inject import org.pf4j.PluginState public class TestPluginExtension : Extension() { - override val name: String = TestPlugin.PLUGIN_ID - - private val pluginManager: PluginManager by inject() - - public inner class ConfigSetArgs : Arguments() { - public val value: String by string { - name = "value" - description = "Value to set. Can be anything." - } - } - - override suspend fun setup() { - publicSlashCommand { - name = "plugin-config" - description = "Commands for working with the test plugin's configuration." - - publicSubCommand { - name = "delete" - description = "Delete current set configuration value." - - action { - TestPlugin.DATA_UNIT - .withUser(event.interaction.user.id) - .delete() - - respond { - content = "User value deleted." - } - } - } - - publicSubCommand { - name = "get" - description = "Get the current set configuration value." - - action { - val value = TestPlugin.DATA_UNIT - .withUser(event.interaction.user.id) - .get() - ?.key - - respond { - content = if (value == null) { - "No user value has been set." - } else { - "**User value:** `$value`" - } - } - } - } - - publicSubCommand(::ConfigSetArgs) { - name = "set" - description = "Set a new configuration value." - - action { - val dataUnit = TestPlugin.DATA_UNIT - .withUserFrom(event) - - val value = dataUnit.get() - ?: TestPluginData(key = arguments.value) - - value.key = arguments.value - - dataUnit.save(value) - - respond { - content = "**User value set:** `${value.key}`" - } - } - } - } - - publicSlashCommand { - name = "plugins" - description = "Retrieve the list of loaded plugins." - - action { - val loadedPlugins = pluginManager.plugins - .filter { it.pluginState == PluginState.STARTED } - .sortedBy { it.descriptor.pluginId } - - val pluginIds = loadedPlugins.map { it.descriptor.pluginId } - - assert(pluginIds.contains(TestPlugin.PLUGIN_ID)) { - "Test plugin (`${TestPlugin.PLUGIN_ID}`) should be loaded." - } - - assert(pluginIds.contains(MappingsPlugin.PLUGIN_ID)) { - "Test plugin (`${MappingsPlugin.PLUGIN_ID}`) should be loaded." - } - - respond { - content = buildString { - appendLine("**${loadedPlugins.size}** loaded plugins.") - appendLine() - - loadedPlugins.forEach { plugin -> - val desc = plugin.descriptor - - appendLine("**»** `${desc.pluginId}@${desc.version}`") - appendLine() - - appendLine( - "**Class:** " + - "`${ - desc.pluginClass.replace( - "com.kotlindiscord.kord.extensions", - "c.k.k.e" - ) - }`" - ) - - appendLine( - "**Dependencies:** `${ - desc.dependencies.joinToString { "`${it.pluginId}`" }.ifEmpty { "None" } - }`" - ) - - appendLine("**License:** `${desc.license}`") - appendLine("**Provider:** `${desc.provider}`") - appendLine("**Requires:** `${desc.requires.ifEmpty { "N/A" }}`") - appendLine() - - desc.pluginDescription.lines().forEach { line -> - appendLine("> $line") - } - - appendLine() - } - } - } - } - } - - publicSlashCommand { - name = "fail-assertion" - description = "Intentionally fail an assertion." - - action { - assert(false) { - "**Assertion failed:** Intentional assertion failure." - } - } - } - } + override val name: String = TestPlugin.PLUGIN_ID + + private val pluginManager: PluginManager by inject() + + public inner class ConfigSetArgs : Arguments() { + public val value: String by string { + name = "value" + description = "Value to set. Can be anything." + } + } + + override suspend fun setup() { + publicSlashCommand { + name = "plugin-config" + description = "Commands for working with the test plugin's configuration." + + publicSubCommand { + name = "delete" + description = "Delete current set configuration value." + + action { + TestPlugin.DATA_UNIT + .withUser(event.interaction.user.id) + .delete() + + respond { + content = "User value deleted." + } + } + } + + publicSubCommand { + name = "get" + description = "Get the current set configuration value." + + action { + val value = TestPlugin.DATA_UNIT + .withUser(event.interaction.user.id) + .get() + ?.key + + respond { + content = if (value == null) { + "No user value has been set." + } else { + "**User value:** `$value`" + } + } + } + } + + publicSubCommand(::ConfigSetArgs) { + name = "set" + description = "Set a new configuration value." + + action { + val dataUnit = TestPlugin.DATA_UNIT + .withUserFrom(event) + + val value = dataUnit.get() + ?: TestPluginData(key = arguments.value) + + value.key = arguments.value + + dataUnit.save(value) + + respond { + content = "**User value set:** `${value.key}`" + } + } + } + } + + publicSlashCommand { + name = "plugins" + description = "Retrieve the list of loaded plugins." + + action { + val loadedPlugins = pluginManager.plugins + .filter { it.pluginState == PluginState.STARTED } + .sortedBy { it.descriptor.pluginId } + + val pluginIds = loadedPlugins.map { it.descriptor.pluginId } + + assert(pluginIds.contains(TestPlugin.PLUGIN_ID)) { + "Test plugin (`${TestPlugin.PLUGIN_ID}`) should be loaded." + } + + assert(pluginIds.contains(MappingsPlugin.PLUGIN_ID)) { + "Test plugin (`${MappingsPlugin.PLUGIN_ID}`) should be loaded." + } + + respond { + content = buildString { + appendLine("**${loadedPlugins.size}** loaded plugins.") + appendLine() + + loadedPlugins.forEach { plugin -> + val desc = plugin.descriptor + + appendLine("**»** `${desc.pluginId}@${desc.version}`") + appendLine() + + appendLine( + "**Class:** " + + "`${ + desc.pluginClass.replace( + "com.kotlindiscord.kord.extensions", + "c.k.k.e" + ) + }`" + ) + + appendLine( + "**Dependencies:** `${ + desc.dependencies.joinToString { "`${it.pluginId}`" }.ifEmpty { "None" } + }`" + ) + + appendLine("**License:** `${desc.license}`") + appendLine("**Provider:** `${desc.provider}`") + appendLine("**Requires:** `${desc.requires.ifEmpty { "N/A" }}`") + appendLine() + + desc.pluginDescription.lines().forEach { line -> + appendLine("> $line") + } + + appendLine() + } + } + } + } + } + + publicSlashCommand { + name = "fail-assertion" + description = "Intentionally fail an assertion." + + action { + assert(false) { + "**Assertion failed:** Intentional assertion failure." + } + } + } + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/LogLevel.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/LogLevel.kt index df7673d57f..4b1254fbc9 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/LogLevel.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/LogLevel.kt @@ -12,48 +12,48 @@ import com.kotlindiscord.kord.extensions.DISCORD_YELLOW import dev.kord.common.Color public sealed class LogLevel( - public val name: String, - public val color: Color?, - private val order: Int + public val name: String, + public val color: Color?, + private val order: Int, ) : Comparable { - public object ERROR : LogLevel("ERROR", DISCORD_RED, 4) - public object WARNING : LogLevel("WARNING", DISCORD_YELLOW, 3) - public object INFO : LogLevel("INFO", DISCORD_BLURPLE, 2) - public object DEBUG : LogLevel("DEBUG", null, 1) - - public fun isEnabled(): Boolean = - this in enabled - - override fun compareTo(other: LogLevel): Int = - order.compareTo(other.order) - - public companion object { - @Suppress("MemberVisibilityCanBePrivate") - public val ALL: Set = setOf(ERROR, WARNING, INFO, DEBUG) - public val WARN: WARNING = WARNING - - public var enabledLevel: LogLevel = INFO - set(value) { - field = value - - enabled = ALL.filter { - it.order >= value.order - }.toSet() - } - - public var enabled: Set = ALL.filter { - it.order >= enabledLevel.order - }.toSet() - - public fun fromString(string: String): LogLevel? = when (string.uppercase()) { - ERROR.toString() -> ERROR - WARNING.toString() -> WARNING - INFO.toString() -> INFO - DEBUG.toString() -> DEBUG - - "WARN" -> WARNING - - else -> null - } - } + public object ERROR : LogLevel("ERROR", DISCORD_RED, 4) + public object WARNING : LogLevel("WARNING", DISCORD_YELLOW, 3) + public object INFO : LogLevel("INFO", DISCORD_BLURPLE, 2) + public object DEBUG : LogLevel("DEBUG", null, 1) + + public fun isEnabled(): Boolean = + this in enabled + + override fun compareTo(other: LogLevel): Int = + order.compareTo(other.order) + + public companion object { + @Suppress("MemberVisibilityCanBePrivate") + public val ALL: Set = setOf(ERROR, WARNING, INFO, DEBUG) + public val WARN: WARNING = WARNING + + public var enabledLevel: LogLevel = INFO + set(value) { + field = value + + enabled = ALL.filter { + it.order >= value.order + }.toSet() + } + + public var enabled: Set = ALL.filter { + it.order >= enabledLevel.order + }.toSet() + + public fun fromString(string: String): LogLevel? = when (string.uppercase()) { + ERROR.toString() -> ERROR + WARNING.toString() -> WARNING + INFO.toString() -> INFO + DEBUG.toString() -> DEBUG + + "WARN" -> WARNING + + else -> null + } + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/_Asserts.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/_Asserts.kt index fbb599d28b..b9cd510752 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/_Asserts.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/_Asserts.kt @@ -14,55 +14,55 @@ import com.kotlindiscord.kord.extensions.commands.CommandContext public typealias AssertBody = (suspend () -> Any)? public suspend fun CommandContext.assert( - value: Boolean, - failureMessage: AssertBody = null + value: Boolean, + failureMessage: AssertBody = null, ) { - if (!value) { - val message = failureMessage?.invoke()?.toString() ?: "Argument is not `true`." + if (!value) { + val message = failureMessage?.invoke()?.toString() ?: "Argument is not `true`." - logError { "**Assertion failed:** $message" } + logError { "**Assertion failed:** $message" } - throw DiscordRelayedException("**Assertion failed:** $message") - } + throw DiscordRelayedException("**Assertion failed:** $message") + } } public suspend fun CommandContext.assertFalse( - value: Boolean, - failureMessage: AssertBody = null + value: Boolean, + failureMessage: AssertBody = null, ) { - if (value) { - val message = failureMessage?.invoke()?.toString() ?: "Argument is not `false`." + if (value) { + val message = failureMessage?.invoke()?.toString() ?: "Argument is not `false`." - logError { "**Assertion failed:** $message" } + logError { "**Assertion failed:** $message" } - throw DiscordRelayedException("**Assertion failed:** $message") - } + throw DiscordRelayedException("**Assertion failed:** $message") + } } public suspend fun CommandContext.assertEqual( - left: Any?, - right: Any?, - failureMessage: AssertBody = null + left: Any?, + right: Any?, + failureMessage: AssertBody = null, ) { - if (left != right) { - val message = failureMessage?.invoke()?.toString() ?: "`$left` is not equal to `$right`" + if (left != right) { + val message = failureMessage?.invoke()?.toString() ?: "`$left` is not equal to `$right`" - logError { "**Assertion failed:** $message" } + logError { "**Assertion failed:** $message" } - throw DiscordRelayedException("**Assertion failed:** $message") - } + throw DiscordRelayedException("**Assertion failed:** $message") + } } public suspend fun CommandContext.assertNotEqual( - left: Any?, - right: Any?, - failureMessage: AssertBody = null + left: Any?, + right: Any?, + failureMessage: AssertBody = null, ) { - if (left == right) { - val message = failureMessage?.invoke()?.toString() ?: "`$left` is equal to `$right`" + if (left == right) { + val message = failureMessage?.invoke()?.toString() ?: "`$left` is equal to `$right`" - logError { "**Assertion failed:** $message" } + logError { "**Assertion failed:** $message" } - throw DiscordRelayedException("**Assertion failed:** $message") - } + throw DiscordRelayedException("**Assertion failed:** $message") + } } diff --git a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/_Logging.kt b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/_Logging.kt index ba9af6245c..c23938e491 100644 --- a/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/_Logging.kt +++ b/test-bot/src/main/kotlin/com/kotlindiscord/kord/extensions/testbot/utils/_Logging.kt @@ -14,7 +14,6 @@ import dev.kord.core.behavior.channel.createMessage import dev.kord.core.entity.Message import dev.kord.core.entity.channel.TextChannel import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.create.embed import dev.kord.rest.builder.message.embed import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -23,88 +22,88 @@ import kotlinx.datetime.Clock public typealias LogBody = (suspend () -> Any?)? public suspend fun Extension.logRaw(builder: MessageCreateBuilder.() -> Unit): Message? { - val channel = kord.getGuildOrNull(TEST_SERVER_ID) - ?.channels - ?.filter { it is TextChannel } - ?.first { - it.name == "test-logs" - } - - return (channel as? TextChannel)?.createMessage(builder) + val channel = kord.getGuildOrNull(TEST_SERVER_ID) + ?.channels + ?.filter { it is TextChannel } + ?.first { + it.name == "test-logs" + } + + return (channel as? TextChannel)?.createMessage(builder) } public suspend fun CommandContext.log(level: LogLevel, body: LogBody = null): Message? { - if (!level.isEnabled()) { - return null - } + if (!level.isEnabled()) { + return null + } - val desc = body?.invoke()?.toString() + val desc = body?.invoke()?.toString() - return command.extension.logRaw { - embed { - this.color = level.color + return command.extension.logRaw { + embed { + this.color = level.color - title = "[${level.name}] Command log: $commandName" - description = desc + title = "[${level.name}] Command log: $commandName" + description = desc - field { - name = "Extension" - value = command.extension.name - } + field { + name = "Extension" + value = command.extension.name + } - timestamp = Clock.System.now() - } - } + timestamp = Clock.System.now() + } + } } public suspend fun CommandContext.logError(body: LogBody = null): Message? = - log(LogLevel.ERROR, body) + log(LogLevel.ERROR, body) public suspend fun CommandContext.logWarning(body: LogBody = null): Message? = - log(LogLevel.WARNING, body) + log(LogLevel.WARNING, body) public suspend fun CommandContext.logInfo(body: LogBody = null): Message? = - log(LogLevel.INFO, body) + log(LogLevel.INFO, body) public suspend fun CommandContext.logDebug(body: LogBody = null): Message? = - log(LogLevel.DEBUG, body) + log(LogLevel.DEBUG, body) public suspend fun EventContext<*>.log( - level: LogLevel, - body: LogBody = null + level: LogLevel, + body: LogBody = null, ): Message? { - if (!level.isEnabled()) { - return null - } + if (!level.isEnabled()) { + return null + } - val desc = body?.invoke()?.toString() - val eventClass = event::class.simpleName + val desc = body?.invoke()?.toString() + val eventClass = event::class.simpleName - return eventHandler.extension.logRaw { - embed { - this.color = level.color + return eventHandler.extension.logRaw { + embed { + this.color = level.color - title = "[${level.name}] Event log: $eventClass" - description = desc + title = "[${level.name}] Event log: $eventClass" + description = desc - field { - name = "Extension" - value = eventHandler.extension.name - } + field { + name = "Extension" + value = eventHandler.extension.name + } - timestamp = Clock.System.now() - } - } + timestamp = Clock.System.now() + } + } } public suspend fun EventContext<*>.logError(body: LogBody = null): Message? = - log(LogLevel.ERROR, body) + log(LogLevel.ERROR, body) public suspend fun EventContext<*>.logWarning(body: LogBody = null): Message? = - log(LogLevel.WARNING, body) + log(LogLevel.WARNING, body) public suspend fun EventContext<*>.logInfo(body: LogBody = null): Message? = - log(LogLevel.INFO, body) + log(LogLevel.INFO, body) public suspend fun EventContext<*>.logDebug(body: LogBody = null): Message? = - log(LogLevel.DEBUG, body) + log(LogLevel.DEBUG, body) diff --git a/test-bot/src/main/resources/logback.groovy b/test-bot/src/main/resources/logback.groovy index fa596f221d..05f9435f33 100644 --- a/test-bot/src/main/resources/logback.groovy +++ b/test-bot/src/main/resources/logback.groovy @@ -17,28 +17,28 @@ if (environment == "dev") { } else if (environment == "spam") { defaultLevel = TRACE - logger("dev.kord.rest.DefaultGateway", TRACE) + logger("dev.kord.rest.DefaultGateway", TRACE) } else { - // Silence warning about missing native PRNG - logger("io.ktor.util.random", ERROR) + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) } appender("CONSOLE", ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" - withJansi = true - } + withJansi = true + } - target = ConsoleTarget.SystemOut + target = ConsoleTarget.SystemOut } appender("FILE", FileAppender) { - file = "output.log" + file = "output.log" - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } } root(defaultLevel, ["CONSOLE", "FILE"]) diff --git a/test-bot/src/main/resources/translations/custom/strings.properties b/test-bot/src/main/resources/translations/custom/strings.properties index abe12d17ad..62b81064e9 100644 --- a/test-bot/src/main/resources/translations/custom/strings.properties +++ b/test-bot/src/main/resources/translations/custom/strings.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - check.simple=This is a check from a different bundle. check.positionalParameters={0} (ID: {1}) can never use this command, even from a different strings bundle. check.namedParameters={user} (ID: {id}) can never use this command, even from a different strings bundle. diff --git a/test-bot/src/main/resources/translations/test/strings.properties b/test-bot/src/main/resources/translations/test/strings.properties index fc03633214..f530eabf7b 100644 --- a/test-bot/src/main/resources/translations/test/strings.properties +++ b/test-bot/src/main/resources/translations/test/strings.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - command.banana=banana command.banana-flat=banana-flat command.banana-sub=banana-sub @@ -20,7 +19,6 @@ check.namedParameters={user} (ID: {id}) can never use this command. validation.simple=Argument validation did not pass. validation.positionalParameters={0} is not valid. validation.namedParameters={value} is not valid. - modal.title=Test Modal modal.line=Line Input modal.line.placeholder=Line Placeholder diff --git a/test-bot/src/main/resources/translations/test/strings_de.properties b/test-bot/src/main/resources/translations/test/strings_de.properties index 34cf4a951f..9d71124776 100644 --- a/test-bot/src/main/resources/translations/test/strings_de.properties +++ b/test-bot/src/main/resources/translations/test/strings_de.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - command.banana=banane command.banana-flat=banane-flat command.banana-sub=banane-sub diff --git a/test-bot/src/main/resources/translations/test/strings_en_GB.properties b/test-bot/src/main/resources/translations/test/strings_en_GB.properties index 1781a51321..6f87a6ee3c 100644 --- a/test-bot/src/main/resources/translations/test/strings_en_GB.properties +++ b/test-bot/src/main/resources/translations/test/strings_en_GB.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - command.banana=banana command.banana-flat=banana-flat command.banana-sub=banana-sub diff --git a/test-bot/src/main/resources/translations/test/strings_en_US.properties b/test-bot/src/main/resources/translations/test/strings_en_US.properties index 1781a51321..6f87a6ee3c 100644 --- a/test-bot/src/main/resources/translations/test/strings_en_US.properties +++ b/test-bot/src/main/resources/translations/test/strings_en_US.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - command.banana=banana command.banana-flat=banana-flat command.banana-sub=banana-sub diff --git a/test-bot/src/main/resources/translations/test/strings_ja.properties b/test-bot/src/main/resources/translations/test/strings_ja.properties index 1de485c845..4f9218bc09 100644 --- a/test-bot/src/main/resources/translations/test/strings_ja.properties +++ b/test-bot/src/main/resources/translations/test/strings_ja.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - command.banana=バナナ command.banana-flat=バナナ-flat command.banana-sub=バナナ-sub diff --git a/test-bot/src/main/resources/translations/test/strings_zh_CN.properties b/test-bot/src/main/resources/translations/test/strings_zh_CN.properties index d9cbe4a4bd..9d808a77e1 100644 --- a/test-bot/src/main/resources/translations/test/strings_zh_CN.properties +++ b/test-bot/src/main/resources/translations/test/strings_zh_CN.properties @@ -3,7 +3,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - command.banana=香蕉 command.banana-flat=香蕉-flat command.banana-sub=香蕉-sub diff --git a/token-parser/build.gradle.kts b/token-parser/build.gradle.kts index 971a6997f5..9ed8e633a5 100644 --- a/token-parser/build.gradle.kts +++ b/token-parser/build.gradle.kts @@ -1,30 +1,30 @@ plugins { - `kordex-module` - `published-module` - `tested-module` + `kordex-module` + `published-module` + `tested-module` } metadata { - name = "KordEx: Token Parser" - description = "Simple token-based command parser, for parsing basic commands from messages sent on chat networks" + name = "KordEx: Token Parser" + description = "Simple token-based command parser, for parsing basic commands from messages sent on chat networks" } dependencies { - implementation(libs.kotlin.stdlib) - implementation(libs.bundles.logging) // Basic logging setup + implementation(libs.kotlin.stdlib) + implementation(libs.bundles.logging) // Basic logging setup - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) + detektPlugins(libs.detekt) + detektPlugins(libs.detekt.libraries) - testApi(libs.kord) - testImplementation(libs.groovy) // For logback config - testImplementation(libs.jansi) - testImplementation(libs.junit) - testImplementation(libs.koin.test) - testImplementation(libs.logback) - testImplementation(libs.logback.groovy) + testApi(libs.kord) + testImplementation(libs.groovy) // For logback config + testImplementation(libs.jansi) + testImplementation(libs.junit) + testImplementation(libs.koin.test) + testImplementation(libs.logback) + testImplementation(libs.logback.groovy) } dokkaModule { - moduleName.set("Kord Extensions: Token Parser") + moduleName.set("Kord Extensions: Token Parser") } diff --git a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/Cursor.kt b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/Cursor.kt index ccaf2b8278..361ecf4374 100644 --- a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/Cursor.kt +++ b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/Cursor.kt @@ -13,130 +13,130 @@ package com.kotlindiscord.kord.extensions.parser * @param input Input string to iterate over. Never modified. */ public class Cursor(public val input: String) { - /** Current iteration index, starting at `-1`. **/ - public var index: Int = -1 - - /** Returns `true` if there are more characters left to iterate over. **/ - public val hasNext: Boolean - get() = - index < input.length - 1 - - /** Returns `true` if there are characters to iterate backwards to. **/ - public val hasPrevious: Boolean - get() = - index > 0 - - /** - * Consume [amount] characters from this cursor, returning them as a [String]. Will stop consuming and return - * when the cursor runs out of characters, instead of throwing. - * - * Returns an empty string if no characters remain. - */ - public fun consumeNumber(amount: Int): String = - buildString { - var total = 0 - - while (hasNext && total < amount) { - append(next()) - - total += 1 - } - } - - /** Iterate over the rest of the string, returning the result. **/ - public fun consumeRemaining(): String = - buildString { - while (hasNext) { - append(next()) - } - } - - /** - * Iterate over the rest of the string as long as the predicate returns `true`, returning the - * result. - */ - public fun consumeWhile(predicate: (Char) -> Boolean): String? { - var result: String? = null - - while (hasNext && predicate(peekNext()!!)) { - result = (result ?: "") + next() - } - - return result - } - - /** Skip any immediate whitespace, updating the [index]. **/ - public fun skipWhitespace(): Boolean { - if (peekNext() != ' ') { - return false - } - - while (peekNext() == ' ') { - next() - } - - return true - } - - /** Increment the [index] and return the character found there, throwing if we're at the end of the string. **/ - public fun next(): Char { - if (hasNext) { - index += 1 - return input[index] - } - - error("Cursor has no further elements.") - } - - /** Increment the [index] and return the character found there, or `null` if we're at the end of the string. **/ - public fun nextOrNull(): Char? { - if (hasNext) { - index += 1 - return input[index] - } - - return null - } - - /** Decrement the [index] and return the character found there, throwing if we're at the start of the string. **/ - public fun previous(): Char { - if (hasPrevious) { - index -= 1 - return input[index] - } - - error("Cursor has no previous elements.") - } - - /** Decrement the [index] and return the character found there, or `null` if we're at the start of the string. **/ - public fun previousOrNull(): Char? { - if (hasPrevious) { - index -= 1 - return input[index] - } - - return null - } - - /** Return the character at the current index. **/ - public fun peek(): Char = - input[index] - - /** Return the character at the next index, or `null` if we're at the end of the string. **/ - public fun peekNext(): Char? { - if (hasNext) { - return input[index + 1] - } - - return null - } - - /** Return the character at the previous index, or `null` if we're at the start of the string. **/ - public fun peekPrevious(): Char? { - if (hasPrevious) { - return input[index - 1] - } - - return null - } + /** Current iteration index, starting at `-1`. **/ + public var index: Int = -1 + + /** Returns `true` if there are more characters left to iterate over. **/ + public val hasNext: Boolean + get() = + index < input.length - 1 + + /** Returns `true` if there are characters to iterate backwards to. **/ + public val hasPrevious: Boolean + get() = + index > 0 + + /** + * Consume [amount] characters from this cursor, returning them as a [String]. Will stop consuming and return + * when the cursor runs out of characters, instead of throwing. + * + * Returns an empty string if no characters remain. + */ + public fun consumeNumber(amount: Int): String = + buildString { + var total = 0 + + while (hasNext && total < amount) { + append(next()) + + total += 1 + } + } + + /** Iterate over the rest of the string, returning the result. **/ + public fun consumeRemaining(): String = + buildString { + while (hasNext) { + append(next()) + } + } + + /** + * Iterate over the rest of the string as long as the predicate returns `true`, returning the + * result. + */ + public fun consumeWhile(predicate: (Char) -> Boolean): String? { + var result: String? = null + + while (hasNext && predicate(peekNext()!!)) { + result = (result ?: "") + next() + } + + return result + } + + /** Skip any immediate whitespace, updating the [index]. **/ + public fun skipWhitespace(): Boolean { + if (peekNext() != ' ') { + return false + } + + while (peekNext() == ' ') { + next() + } + + return true + } + + /** Increment the [index] and return the character found there, throwing if we're at the end of the string. **/ + public fun next(): Char { + if (hasNext) { + index += 1 + return input[index] + } + + error("Cursor has no further elements.") + } + + /** Increment the [index] and return the character found there, or `null` if we're at the end of the string. **/ + public fun nextOrNull(): Char? { + if (hasNext) { + index += 1 + return input[index] + } + + return null + } + + /** Decrement the [index] and return the character found there, throwing if we're at the start of the string. **/ + public fun previous(): Char { + if (hasPrevious) { + index -= 1 + return input[index] + } + + error("Cursor has no previous elements.") + } + + /** Decrement the [index] and return the character found there, or `null` if we're at the start of the string. **/ + public fun previousOrNull(): Char? { + if (hasPrevious) { + index -= 1 + return input[index] + } + + return null + } + + /** Return the character at the current index. **/ + public fun peek(): Char = + input[index] + + /** Return the character at the next index, or `null` if we're at the end of the string. **/ + public fun peekNext(): Char? { + if (hasNext) { + return input[index + 1] + } + + return null + } + + /** Return the character at the previous index, or `null` if we're at the start of the string. **/ + public fun peekPrevious(): Char? { + if (hasPrevious) { + return input[index - 1] + } + + return null + } } diff --git a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/StringParser.kt b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/StringParser.kt index eec2488104..b33bdaa4ef 100644 --- a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/StringParser.kt +++ b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/StringParser.kt @@ -31,336 +31,336 @@ import io.github.oshai.kotlinlogging.KotlinLogging */ @Suppress("StringLiteralDuplication") // That'll happen in a parser like this public open class StringParser(public open val input: String) { - private val logger = KotlinLogging.logger { } + private val logger = KotlinLogging.logger { } - /** - * Cursor object, representing the current parsing state. Initially, this will contain a [Cursor] that iterates - * over the entire [input], but functions (eg [parseNamed]) may reassign it to change the iteration target. - */ - public var cursor: Cursor = Cursor(input) + /** + * Cursor object, representing the current parsing state. Initially, this will contain a [Cursor] that iterates + * over the entire [input], but functions (eg [parseNamed]) may reassign it to change the iteration target. + */ + public var cursor: Cursor = Cursor(input) - /** Returns `true` if the [cursor] has more parsing to do. **/ - public val hasNext: Boolean get() = cursor.hasNext + /** Returns `true` if the [cursor] has more parsing to do. **/ + public val hasNext: Boolean get() = cursor.hasNext - /** - * Parse all of the flag and keyword arguments out of the [cursor], creating a new [Cursor] containing only the - * positional arguments and returning a list of parsed [NamedArgumentToken]s. - * - * **Note:** This function reassigns the [cursor] property! - */ - public fun parseNamed(): List { - val tokens: MutableList = mutableListOf() + /** + * Parse all of the flag and keyword arguments out of the [cursor], creating a new [Cursor] containing only the + * positional arguments and returning a list of parsed [NamedArgumentToken]s. + * + * **Note:** This function reassigns the [cursor] property! + */ + public fun parseNamed(): List { + val tokens: MutableList = mutableListOf() - val buffer = StringBuilder() - val outputBuffer = StringBuilder() + val buffer = StringBuilder() + val outputBuffer = StringBuilder() - var isQuoted = false + var isQuoted = false - var isFlag = false - var isFlagValue = false - var flagName = "" + var isFlag = false + var isFlagValue = false + var flagName = "" - var isKeyword = false - var keywordName = "" + var isKeyword = false + var keywordName = "" - @Suppress("LoopWithTooManyJumpStatements") // Tell me I suck, why don't you - while (cursor.hasNext) { - val char = cursor.next() + @Suppress("LoopWithTooManyJumpStatements") // Tell me I suck, why don't you + while (cursor.hasNext) { + val char = cursor.next() - logger.trace { "Character: $char" } + logger.trace { "Character: $char" } - val canBeQuoted = !isQuoted && - (!(isFlag && !isFlag) || isKeyword) + val canBeQuoted = !isQuoted && + (!(isFlag && !isFlagValue) || isKeyword) - if (char == '"' && buffer.isEmpty() && canBeQuoted) { - // Nothing in the buffer, opening quote - ignore spaces as we continue - logger.trace { " Marking as quoted." } + if (char == '"' && buffer.isEmpty() && canBeQuoted) { + // Nothing in the buffer, opening quote - ignore spaces as we continue + logger.trace { " Marking as quoted." } - isQuoted = true - continue - } + isQuoted = true + continue + } - if (char == '-' && cursor.peekNext() == '-' && buffer.isEmpty() && !isFlag && !isKeyword) { - // This is a flag, --key value type deal - logger.trace { " Marking as flag." } + if (char == '-' && cursor.peekNext() == '-' && buffer.isEmpty() && !isFlag && !isKeyword) { + // This is a flag, --key value type deal + logger.trace { " Marking as flag." } - cursor.next() - isFlag = true - continue - } + cursor.next() + isFlag = true + continue + } - if (char == '=' && buffer.isNotEmpty() && !isKeyword && !isFlag) { - // Keyword pair, key=value - logger.trace { " Marking as keyword pair." } + if (char == '=' && buffer.isNotEmpty() && !isKeyword && !isFlag) { + // Keyword pair, key=value + logger.trace { " Marking as keyword pair." } - keywordName = buffer.toString() - buffer.clear() + keywordName = buffer.toString() + buffer.clear() - isKeyword = true - continue - } + isKeyword = true + continue + } - if (char == '\\' && cursor.peekNext() == '"' && isQuoted) { - // Escaped quote, only handle if it's in a quoted argument though - logger.trace { " Escaped quote." } + if (char == '\\' && cursor.peekNext() == '"' && isQuoted) { + // Escaped quote, only handle if it's in a quoted argument though + logger.trace { " Escaped quote." } - buffer.append('"') - cursor.next() - continue - } + buffer.append('"') + cursor.next() + continue + } - if (char == '"' && isQuoted) { - // We're at the end of this part of the token - logger.trace { " Reached quoted end." } + if (char == '"' && isQuoted) { + // We're at the end of this part of the token + logger.trace { " Reached quoted end." } - if (isFlagValue) { - // Flag names can't have spaces - logger.trace { " Flag value detected." } - logger.trace { "" } + if (isFlagValue) { + // Flag names can't have spaces + logger.trace { " Flag value detected." } + logger.trace { "" } - tokens.add(NamedArgumentToken(flagName, buffer.toString())) + tokens.add(NamedArgumentToken(flagName, buffer.toString())) - flagName = "" + flagName = "" - isFlag = false - isFlagValue = false - } else if (isKeyword) { - // Keyword names can't have spaces either - logger.trace { " Keyword value detected." } - logger.trace { "" } + isFlag = false + isFlagValue = false + } else if (isKeyword) { + // Keyword names can't have spaces either + logger.trace { " Keyword value detected." } + logger.trace { "" } - tokens.add(NamedArgumentToken(keywordName, buffer.toString())) + tokens.add(NamedArgumentToken(keywordName, buffer.toString())) - keywordName = "" + keywordName = "" - isKeyword = false - } else { - // Not a flag/keyword value, so we're at the end of this token - logger.trace { " Token end detected." } - logger.trace { "" } + isKeyword = false + } else { + // Not a flag/keyword value, so we're at the end of this token + logger.trace { " Token end detected." } + logger.trace { "" } - outputBuffer.append("\"$buffer\"") - cursor.skipWhitespace() - } + outputBuffer.append("\"$buffer\"") + cursor.skipWhitespace() + } - buffer.clear() - isQuoted = false + buffer.clear() + isQuoted = false - continue - } + continue + } - if (char == ' ' && !isQuoted) { - // Not quoted, so we're at the end of this part of the token - logger.trace { " Whitespace detected." } + if (char == ' ' && !isQuoted) { + // Not quoted, so we're at the end of this part of the token + logger.trace { " Whitespace detected." } - if (isFlag) { - // Could be a flag name or value, they're space-separated + if (isFlag) { + // Could be a flag name or value, they're space-separated - if (!isFlagValue) { - logger.trace { " Flag name detected." } - logger.trace { "" } + if (!isFlagValue) { + logger.trace { " Flag name detected." } + logger.trace { "" } - flagName = buffer.toString() - isFlagValue = true - } else { - logger.trace { " Flag value detected." } - logger.trace { "" } + flagName = buffer.toString() + isFlagValue = true + } else { + logger.trace { " Flag value detected." } + logger.trace { "" } - tokens.add(NamedArgumentToken(flagName, buffer.toString())) - cursor.skipWhitespace() + tokens.add(NamedArgumentToken(flagName, buffer.toString())) + cursor.skipWhitespace() - flagName = "" + flagName = "" - isFlag = false - isFlagValue = false - } - } else if (isKeyword) { - // This is a keyword value - logger.trace { " Keyword value detected." } - logger.trace { "" } + isFlag = false + isFlagValue = false + } + } else if (isKeyword) { + // This is a keyword value + logger.trace { " Keyword value detected." } + logger.trace { "" } - tokens.add(NamedArgumentToken(keywordName, buffer.toString())) - cursor.skipWhitespace() + tokens.add(NamedArgumentToken(keywordName, buffer.toString())) + cursor.skipWhitespace() - keywordName = "" + keywordName = "" - isKeyword = false - } else { - // Not a flag/keyword value, so we're at the end of this token - logger.trace { " Token end detected." } - logger.trace { "" } + isKeyword = false + } else { + // Not a flag/keyword value, so we're at the end of this token + logger.trace { " Token end detected." } + logger.trace { "" } - outputBuffer.append("$buffer ") + outputBuffer.append("$buffer ") // cursor.skipWhitespace() - } + } - buffer.clear() - continue - } + buffer.clear() + continue + } - buffer.append(char) - logger.trace { " Adding: \"$buffer\" + '$char'" } - } + buffer.append(char) + logger.trace { " Adding: \"$buffer\" + '$char'" } + } - if (buffer.isNotEmpty()) { - logger.trace { "" } - logger.trace { "Buffer's not empty yet." } + if (buffer.isNotEmpty()) { + logger.trace { "" } + logger.trace { "Buffer's not empty yet." } - if (isFlag) { - // Could be a flag name or value, they're space-separated + if (isFlag) { + // Could be a flag name or value, they're space-separated - if (!isFlagValue) { - logger.trace { " !! Flag name detected - this shouldn't happen!" } - } else { - logger.trace { " Flag value detected." } + if (!isFlagValue) { + logger.trace { " !! Flag name detected - this shouldn't happen!" } + } else { + logger.trace { " Flag value detected." } - tokens.add(NamedArgumentToken(flagName, buffer.toString())) - } - } else if (isKeyword) { - // This is a keyword value - logger.trace { " Keyword value detected." } + tokens.add(NamedArgumentToken(flagName, buffer.toString())) + } + } else if (isKeyword) { + // This is a keyword value + logger.trace { " Keyword value detected." } - tokens.add(NamedArgumentToken(keywordName, buffer.toString())) - } else { - // Not a flag/keyword value, so we're at the end of this token - logger.trace { " End of token detected." } + tokens.add(NamedArgumentToken(keywordName, buffer.toString())) + } else { + // Not a flag/keyword value, so we're at the end of this token + logger.trace { " End of token detected." } - outputBuffer.append(buffer.toString()) - } - } + outputBuffer.append(buffer.toString()) + } + } - cursor = Cursor(outputBuffer.toString().trim()) + cursor = Cursor(outputBuffer.toString().trim()) - return tokens - } + return tokens + } - /** - * Attempt to parse the next token and reset the cursor's index to what it was before parsing, before returning - * the result. - */ - public fun peekNext(): PositionalArgumentToken? { - val curIndex = cursor.index - val token = parseNext() + /** + * Attempt to parse the next token and reset the cursor's index to what it was before parsing, before returning + * the result. + */ + public fun peekNext(): PositionalArgumentToken? { + val curIndex = cursor.index + val token = parseNext() - cursor.index = curIndex - - return token - } + cursor.index = curIndex + + return token + } - /** - * Attempt to parse a single or quoted positional argument token from the [cursor], returning it if there was a - * token, or `null` if there wasn't anything left to parse. - */ - public fun parseNext(): PositionalArgumentToken? { - var token: PositionalArgumentToken? = null - var buffer = StringBuilder() + /** + * Attempt to parse a single or quoted positional argument token from the [cursor], returning it if there was a + * token, or `null` if there wasn't anything left to parse. + */ + public fun parseNext(): PositionalArgumentToken? { + val buffer = StringBuilder() - var isQuoted = false - - @Suppress("LoopWithTooManyJumpStatements") // rude - while (cursor.hasNext) { - val char = cursor.next() + var token: PositionalArgumentToken? = null + var isQuoted = false + + @Suppress("LoopWithTooManyJumpStatements") // rude + while (cursor.hasNext) { + val char = cursor.next() - if (char == '"' && buffer.isEmpty() && !isQuoted) { - // Nothing in the buffer, opening quote - ignore spaces as we continue - logger.trace { " Marking as quoted." } + if (char == '"' && buffer.isEmpty() && !isQuoted) { + // Nothing in the buffer, opening quote - ignore spaces as we continue + logger.trace { " Marking as quoted." } - isQuoted = true - continue - } + isQuoted = true + continue + } - if (char == '\\' && cursor.peekNext() == '"' && isQuoted) { - // Escaped quote, only handle if it's in a quoted argument though - logger.trace { " Escaped quote." } + if (char == '\\' && cursor.peekNext() == '"' && isQuoted) { + // Escaped quote, only handle if it's in a quoted argument though + logger.trace { " Escaped quote." } - buffer.append('"') - cursor.next() - continue - } + buffer.append('"') + cursor.next() + continue + } - if (char == '"' && isQuoted) { - // We're at the end of this part of the token - logger.trace { " Reached quoted end." } - logger.trace { " Token end detected." } - logger.trace { "" } + if (char == '"' && isQuoted) { + // We're at the end of this part of the token + logger.trace { " Reached quoted end." } + logger.trace { " Token end detected." } + logger.trace { "" } - token = PositionalArgumentToken(buffer.toString()) - cursor.skipWhitespace() + token = PositionalArgumentToken(buffer.toString()) + cursor.skipWhitespace() - buffer.clear() + buffer.clear() - break - } + break + } - if (char == ' ' && !isQuoted) { - // Not quoted, so we're at the end of this part of the token - logger.trace { " Whitespace detected." } - logger.trace { " Token end detected." } - logger.trace { "" } + if (char == ' ' && !isQuoted) { + // Not quoted, so we're at the end of this part of the token + logger.trace { " Whitespace detected." } + logger.trace { " Token end detected." } + logger.trace { "" } - token = PositionalArgumentToken(buffer.toString()) - cursor.skipWhitespace() + token = PositionalArgumentToken(buffer.toString()) + cursor.skipWhitespace() - buffer.clear() - - break - } + buffer.clear() + + break + } - buffer.append(char) - logger.trace { " Adding: \"$buffer\" + '$char'" } - } - - if (buffer.isNotEmpty()) { - logger.trace { "" } - logger.trace { "Remaining buffer treated as positional token." } + buffer.append(char) + logger.trace { " Adding: \"$buffer\" + '$char'" } + } + + if (buffer.isNotEmpty()) { + logger.trace { "" } + logger.trace { "Remaining buffer treated as positional token." } - token = PositionalArgumentToken(buffer.toString()) - } + token = PositionalArgumentToken(buffer.toString()) + } - return token - } + return token + } - /** Consume whatever is left in the [cursor], setting it to the end of its input string. **/ - public fun consumeRemaining(): String = cursor.consumeRemaining() + /** Consume whatever is left in the [cursor], setting it to the end of its input string. **/ + public fun consumeRemaining(): String = cursor.consumeRemaining() - /** - * Consume characters from the [cursor] while the [predicate] returns `true`, and return those characters joined - * into a String. If the predicate fails on the first character, `null` will be returned instead. - * - * **Note:** Once this function has finished consuming characters, it will instruct the cursor to skip any - * immediate whitespace, which prepares it for normal token parsing - assuming there's anything left to parse. - */ - public fun consumeWhile(predicate: (Char) -> Boolean): String? { - val result = cursor.consumeWhile(predicate) + /** + * Consume characters from the [cursor] while the [predicate] returns `true`, and return those characters joined + * into a String. If the predicate fails on the first character, `null` will be returned instead. + * + * **Note:** Once this function has finished consuming characters, it will instruct the cursor to skip any + * immediate whitespace, which prepares it for normal token parsing - assuming there's anything left to parse. + */ + public fun consumeWhile(predicate: (Char) -> Boolean): String? { + val result = cursor.consumeWhile(predicate) - if (result != null) { - cursor.skipWhitespace() - } + if (result != null) { + cursor.skipWhitespace() + } - return result - } + return result + } - /** Return whatever remains in the [cursor]'s string, without consuming it'. **/ - public fun peekRemaining(): String { - val curIndex = cursor.index - val result = cursor.consumeRemaining() + /** Return whatever remains in the [cursor]'s string, without consuming it'. **/ + public fun peekRemaining(): String { + val curIndex = cursor.index + val result = cursor.consumeRemaining() - cursor.index = curIndex + cursor.index = curIndex - return result - } + return result + } - /** - * Collect characters from the [cursor] while the [predicate] returns `true`, and return those characters joined - * into a String. If the predicate fails on the first character, `null` will be returned instead. After - * characters have been collected, the cursor's index is reset, so the characters won't be consumed. - */ - public fun peekWhile(predicate: (Char) -> Boolean): String? { - val curIndex = cursor.index - val result = cursor.consumeWhile(predicate) + /** + * Collect characters from the [cursor] while the [predicate] returns `true`, and return those characters joined + * into a String. If the predicate fails on the first character, `null` will be returned instead. After + * characters have been collected, the cursor's index is reset, so the characters won't be consumed. + */ + public fun peekWhile(predicate: (Char) -> Boolean): String? { + val curIndex = cursor.index + val result = cursor.consumeWhile(predicate) - cursor.index = curIndex + cursor.index = curIndex - return result - } + return result + } } diff --git a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/NamedArgumentToken.kt b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/NamedArgumentToken.kt index c54227375e..2955b6dcec 100644 --- a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/NamedArgumentToken.kt +++ b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/NamedArgumentToken.kt @@ -13,6 +13,6 @@ package com.kotlindiscord.kord.extensions.parser.tokens * @param data Argument data */ public data class NamedArgumentToken( - public val name: String, - override val data: String + public val name: String, + override val data: String, ) : Token diff --git a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/PositionalArgumentToken.kt b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/PositionalArgumentToken.kt index ac10f0fdc6..f0573b01d0 100644 --- a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/PositionalArgumentToken.kt +++ b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/PositionalArgumentToken.kt @@ -12,5 +12,5 @@ package com.kotlindiscord.kord.extensions.parser.tokens * @param data Argument data */ public data class PositionalArgumentToken( - override val data: String + override val data: String, ) : Token diff --git a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/Token.kt b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/Token.kt index ae90780d96..ed316806a6 100644 --- a/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/Token.kt +++ b/token-parser/src/main/kotlin/com/kotlindiscord/kord/extensions/parser/tokens/Token.kt @@ -10,6 +10,6 @@ package com.kotlindiscord.kord.extensions.parser.tokens * Simple base class for a parser token. Exists in order to make changes later easier. */ public interface Token { - /** Stored token data. **/ - public val data: T + /** Stored token data. **/ + public val data: T } diff --git a/token-parser/src/test/kotlin/com/kotlindiscord/kord/extensions/test/StringParserTest.kt b/token-parser/src/test/kotlin/com/kotlindiscord/kord/extensions/test/StringParserTest.kt index ac90cc0154..e37b507f00 100644 --- a/token-parser/src/test/kotlin/com/kotlindiscord/kord/extensions/test/StringParserTest.kt +++ b/token-parser/src/test/kotlin/com/kotlindiscord/kord/extensions/test/StringParserTest.kt @@ -27,164 +27,164 @@ const val NUMBERS = "12345" @TestInstance(TestInstance.Lifecycle.PER_CLASS) class StringParserTest { - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Consuming - everything`() = runBlocking { - val parser = StringParser(NUMBERS) - val everything = parser.consumeRemaining() + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Consuming - everything`() = runBlocking { + val parser = StringParser(NUMBERS) + val everything = parser.consumeRemaining() - assertEquals(NUMBERS, everything) + assertEquals(NUMBERS, everything) - assertEquals("", parser.consumeRemaining()) - assertEquals(null, parser.parseNext()) - } + assertEquals("", parser.consumeRemaining()) + assertEquals(null, parser.parseNext()) + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Consuming - predicates`() = runBlocking { - val parser = StringParser(NUMBERS) + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Consuming - predicates`() = runBlocking { + val parser = StringParser(NUMBERS) - val one = parser.consumeWhile { it == '1' } - val two = parser.consumeWhile { it == '2' } - val token = parser.parseNext() + val one = parser.consumeWhile { it == '1' } + val two = parser.consumeWhile { it == '2' } + val token = parser.parseNext() - assertEquals("1", one) - assertEquals("2", two) - assertEquals(null, parser.consumeWhile { it == '2' }) + assertEquals("1", one) + assertEquals("2", two) + assertEquals(null, parser.consumeWhile { it == '2' }) - assertEquals("345", token?.data) + assertEquals("345", token?.data) - assertEquals("", parser.consumeRemaining()) - assertEquals(null, parser.parseNext()) - } + assertEquals("", parser.consumeRemaining()) + assertEquals(null, parser.parseNext()) + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Everything - natural order`() = runBlocking { - val parser = StringParser(EVERYTHING_INPUT) + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Everything - natural order`() = runBlocking { + val parser = StringParser(EVERYTHING_INPUT) - val namedTokens: MutableList = mutableListOf() - val positionalTokens: MutableList = mutableListOf() + val namedTokens: MutableList = mutableListOf() + val positionalTokens: MutableList = mutableListOf() - namedTokens.addAll(parser.parseNamed()) + namedTokens.addAll(parser.parseNamed()) - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) - assertEquals(2, namedTokens.size) { "Parser should find two named tokens" } - assertEquals(2, positionalTokens.size) { "Parser should find two positional tokens" } + assertEquals(2, namedTokens.size) { "Parser should find two named tokens" } + assertEquals(2, positionalTokens.size) { "Parser should find two positional tokens" } - assertNull(parser.parseNext(), "Parser should not have anything else to parse.") + assertNull(parser.parseNext(), "Parser should not have anything else to parse.") - assertEquals("key", namedTokens[0].name) { "Named 1: name = key" } - assertEquals("value with quotes", namedTokens[0].data) { "Named 1: data = value with quotes" } + assertEquals("key", namedTokens[0].name) { "Named 1: name = key" } + assertEquals("value with quotes", namedTokens[0].data) { "Named 1: data = value with quotes" } - assertEquals("name", namedTokens[1].name) { "Named 2: name = name" } - assertEquals("value", namedTokens[1].data) { "Named 2: data = value" } + assertEquals("name", namedTokens[1].name) { "Named 2: name = name" } + assertEquals("value", namedTokens[1].data) { "Named 2: data = value" } - assertEquals("single", positionalTokens[0].data) { "Positional 1: data = single" } - assertEquals("with quotes", positionalTokens[1].data) { "Positional 2: data = with quotes" } - } + assertEquals("single", positionalTokens[0].data) { "Positional 1: data = single" } + assertEquals("with quotes", positionalTokens[1].data) { "Positional 2: data = with quotes" } + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Everything - named first`() = runBlocking { - val parser = StringParser(EVERYTHING_INPUT_NAMED_FIRST) + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Everything - named first`() = runBlocking { + val parser = StringParser(EVERYTHING_INPUT_NAMED_FIRST) - val namedTokens: MutableList = mutableListOf() - val positionalTokens: MutableList = mutableListOf() + val namedTokens: MutableList = mutableListOf() + val positionalTokens: MutableList = mutableListOf() - namedTokens.addAll(parser.parseNamed()) + namedTokens.addAll(parser.parseNamed()) - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) - assertEquals(2, namedTokens.size) { "Parser should find two named tokens" } - assertEquals(2, positionalTokens.size) { "Parser should find two positional tokens" } + assertEquals(2, namedTokens.size) { "Parser should find two named tokens" } + assertEquals(2, positionalTokens.size) { "Parser should find two positional tokens" } - assertNull(parser.parseNext(), "Parser should not have anything else to parse.") + assertNull(parser.parseNext(), "Parser should not have anything else to parse.") - assertEquals("key", namedTokens[0].name) { "Named 1: name = key" } - assertEquals("value with quotes", namedTokens[0].data) { "Named 1: data = value with quotes" } + assertEquals("key", namedTokens[0].name) { "Named 1: name = key" } + assertEquals("value with quotes", namedTokens[0].data) { "Named 1: data = value with quotes" } - assertEquals("name", namedTokens[1].name) { "Named 2: name = name" } - assertEquals("value", namedTokens[1].data) { "Named 2: data = value" } + assertEquals("name", namedTokens[1].name) { "Named 2: name = name" } + assertEquals("value", namedTokens[1].data) { "Named 2: data = value" } - assertEquals("single", positionalTokens[0].data) { "Positional 1: data = single" } - assertEquals("with quotes", positionalTokens[1].data) { "Positional 2: data = with quotes" } - } + assertEquals("single", positionalTokens[0].data) { "Positional 1: data = single" } + assertEquals("with quotes", positionalTokens[1].data) { "Positional 2: data = with quotes" } + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Single positional arguments`() = runBlocking { - val parser = StringParser(SINGLE_INPUT) + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Single positional arguments`() = runBlocking { + val parser = StringParser(SINGLE_INPUT) - val positionalTokens: MutableList = mutableListOf() + val positionalTokens: MutableList = mutableListOf() - assertEquals(0, parser.parseNamed().size) { "Parser should find no named tokens" } + assertEquals(0, parser.parseNamed().size) { "Parser should find no named tokens" } - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) - assertEquals("one", positionalTokens[0].data) { "Positional 1: data = one" } - assertEquals("two", positionalTokens[1].data) { "Positional 2: data = two" } - assertEquals("three", positionalTokens[2].data) { "Positional 3: data = three" } - assertEquals("four", positionalTokens[3].data) { "Positional 4: data = four" } - assertEquals("five", positionalTokens[4].data) { "Positional 5: data = five" } + assertEquals("one", positionalTokens[0].data) { "Positional 1: data = one" } + assertEquals("two", positionalTokens[1].data) { "Positional 2: data = two" } + assertEquals("three", positionalTokens[2].data) { "Positional 3: data = three" } + assertEquals("four", positionalTokens[3].data) { "Positional 4: data = four" } + assertEquals("five", positionalTokens[4].data) { "Positional 5: data = five" } - assertNull(parser.parseNext(), "Parser should not have anything else to parse.") - } + assertNull(parser.parseNext(), "Parser should not have anything else to parse.") + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Quoted positional arguments`() = runBlocking { - val parser = StringParser(QUOTED_INPUT) + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Quoted positional arguments`() = runBlocking { + val parser = StringParser(QUOTED_INPUT) - val positionalTokens: MutableList = mutableListOf() + val positionalTokens: MutableList = mutableListOf() - assertEquals(0, parser.parseNamed().size) { "Parser should find no named tokens" } + assertEquals(0, parser.parseNamed().size) { "Parser should find no named tokens" } - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) - positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) + positionalTokens.add(parser.parseNext()!!) - assertEquals("one one", positionalTokens[0].data) { "Positional 1: data = one one" } - assertEquals("two two", positionalTokens[1].data) { "Positional 2: data = two two" } - assertEquals("three three", positionalTokens[2].data) { "Positional 3: data = three three" } - assertEquals("four four", positionalTokens[3].data) { "Positional 4: data = four four" } - assertEquals("five five", positionalTokens[4].data) { "Positional 5: data = five five" } + assertEquals("one one", positionalTokens[0].data) { "Positional 1: data = one one" } + assertEquals("two two", positionalTokens[1].data) { "Positional 2: data = two two" } + assertEquals("three three", positionalTokens[2].data) { "Positional 3: data = three three" } + assertEquals("four four", positionalTokens[3].data) { "Positional 4: data = four four" } + assertEquals("five five", positionalTokens[4].data) { "Positional 5: data = five five" } - assertNull(parser.parseNext(), "Parser should not have anything else to parse.") - } + assertNull(parser.parseNext(), "Parser should not have anything else to parse.") + } - @Test - @Execution(ExecutionMode.CONCURRENT) - fun `Named arguments only`() = runBlocking { - val parser = StringParser(NAMED_INPUT) + @Test + @Execution(ExecutionMode.CONCURRENT) + fun `Named arguments only`() = runBlocking { + val parser = StringParser(NAMED_INPUT) - val namedTokens: MutableList = mutableListOf() + val namedTokens: MutableList = mutableListOf() - namedTokens.addAll(parser.parseNamed()) + namedTokens.addAll(parser.parseNamed()) - assertEquals(4, namedTokens.size) { "Parser should find four named tokens" } + assertEquals(4, namedTokens.size) { "Parser should find four named tokens" } - assertEquals("one", namedTokens[0].name) { "Token 1: name = one" } - assertEquals("one", namedTokens[0].data) { "Token 1: data = one" } + assertEquals("one", namedTokens[0].name) { "Token 1: name = one" } + assertEquals("one", namedTokens[0].data) { "Token 1: data = one" } - assertEquals("two", namedTokens[1].name) { "Token 2: name = two" } - assertEquals("two", namedTokens[1].data) { "Token 2: data = two" } + assertEquals("two", namedTokens[1].name) { "Token 2: name = two" } + assertEquals("two", namedTokens[1].data) { "Token 2: data = two" } - assertEquals("three", namedTokens[2].name) { "Token 3: name = three" } - assertEquals("three three", namedTokens[2].data) { "Token 3: data = three three" } + assertEquals("three", namedTokens[2].name) { "Token 3: name = three" } + assertEquals("three three", namedTokens[2].data) { "Token 3: data = three three" } - assertEquals("four", namedTokens[3].name) { "Token 4: name = four" } - assertEquals("four four", namedTokens[3].data) { "Token 4: data = four four" } + assertEquals("four", namedTokens[3].name) { "Token 4: name = four" } + assertEquals("four four", namedTokens[3].data) { "Token 4: data = four four" } - assertNull(parser.parseNext(), "Parser should not have anything else to parse.") - } + assertNull(parser.parseNext(), "Parser should not have anything else to parse.") + } } diff --git a/token-parser/src/test/resources/junit-platform.properties b/token-parser/src/test/resources/junit-platform.properties index 580f511dad..6939a7fb02 100644 --- a/token-parser/src/test/resources/junit-platform.properties +++ b/token-parser/src/test/resources/junit-platform.properties @@ -3,5 +3,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. # - junit.jupiter.execution.parallel.enabled=true diff --git a/token-parser/src/test/resources/logback.groovy b/token-parser/src/test/resources/logback.groovy index a0c69989f4..22a2ceece2 100644 --- a/token-parser/src/test/resources/logback.groovy +++ b/token-parser/src/test/resources/logback.groovy @@ -4,6 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + import ch.qos.logback.classic.encoder.PatternLayoutEncoder import ch.qos.logback.core.joran.spi.ConsoleTarget import ch.qos.logback.core.ConsoleAppender @@ -13,28 +14,28 @@ def environment = System.getenv("ENVIRONMENT") ?: "dev" def defaultLevel = TRACE if (environment == "spam") { - logger("dev.kord.rest.DefaultGateway", TRACE) + logger("dev.kord.rest.DefaultGateway", TRACE) } else { - // Silence warning about missing native PRNG - logger("io.ktor.util.random", ERROR) + // Silence warning about missing native PRNG + logger("io.ktor.util.random", ERROR) } appender("CONSOLE", ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" + encoder(PatternLayoutEncoder) { + pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n" - withJansi = true - } + withJansi = true + } - target = ConsoleTarget.SystemOut + target = ConsoleTarget.SystemOut } appender("FILE", FileAppender) { - file = "output.log" + file = "output.log" - encoder(PatternLayoutEncoder) { - pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n" + } } root(defaultLevel, ["CONSOLE", "FILE"])