@azurioh/discord-kernel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/authz/authorizer.d.ts +45 -0
- package/dist/authz/authorizer.d.ts.map +1 -0
- package/dist/authz/authorizer.js +31 -0
- package/dist/authz/authorizer.js.map +1 -0
- package/dist/authz/require-level-guard.d.ts +23 -0
- package/dist/authz/require-level-guard.d.ts.map +1 -0
- package/dist/authz/require-level-guard.js +37 -0
- package/dist/authz/require-level-guard.js.map +1 -0
- package/dist/clock.d.ts +13 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +13 -0
- package/dist/clock.js.map +1 -0
- package/dist/concurrency/keyed-queue.d.ts +18 -0
- package/dist/concurrency/keyed-queue.d.ts.map +1 -0
- package/dist/concurrency/keyed-queue.js +37 -0
- package/dist/concurrency/keyed-queue.js.map +1 -0
- package/dist/concurrency/run-with-concurrency.d.ts +15 -0
- package/dist/concurrency/run-with-concurrency.d.ts.map +1 -0
- package/dist/concurrency/run-with-concurrency.js +40 -0
- package/dist/concurrency/run-with-concurrency.js.map +1 -0
- package/dist/config/env.d.ts +17 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +49 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/errors.d.ts +14 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +25 -0
- package/dist/config/errors.js.map +1 -0
- package/dist/datetime.d.ts +34 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +188 -0
- package/dist/datetime.js.map +1 -0
- package/dist/discord/api-errors.d.ts +20 -0
- package/dist/discord/api-errors.d.ts.map +1 -0
- package/dist/discord/api-errors.js +90 -0
- package/dist/discord/api-errors.js.map +1 -0
- package/dist/discord/channel-exporter.d.ts +42 -0
- package/dist/discord/channel-exporter.d.ts.map +1 -0
- package/dist/discord/channel-exporter.js +3 -0
- package/dist/discord/channel-exporter.js.map +1 -0
- package/dist/discord/command/autocomplete-dispatcher.d.ts +26 -0
- package/dist/discord/command/autocomplete-dispatcher.d.ts.map +1 -0
- package/dist/discord/command/autocomplete-dispatcher.js +41 -0
- package/dist/discord/command/autocomplete-dispatcher.js.map +1 -0
- package/dist/discord/command/autocomplete.d.ts +18 -0
- package/dist/discord/command/autocomplete.d.ts.map +1 -0
- package/dist/discord/command/autocomplete.js +65 -0
- package/dist/discord/command/autocomplete.js.map +1 -0
- package/dist/discord/command/context.d.ts +33 -0
- package/dist/discord/command/context.d.ts.map +1 -0
- package/dist/discord/command/context.js +48 -0
- package/dist/discord/command/context.js.map +1 -0
- package/dist/discord/command/cooldown-guard.d.ts +25 -0
- package/dist/discord/command/cooldown-guard.d.ts.map +1 -0
- package/dist/discord/command/cooldown-guard.js +60 -0
- package/dist/discord/command/cooldown-guard.js.map +1 -0
- package/dist/discord/command/create-command.d.ts +100 -0
- package/dist/discord/command/create-command.d.ts.map +1 -0
- package/dist/discord/command/create-command.js +245 -0
- package/dist/discord/command/create-command.js.map +1 -0
- package/dist/discord/command/create-context-menu-command.d.ts +51 -0
- package/dist/discord/command/create-context-menu-command.d.ts.map +1 -0
- package/dist/discord/command/create-context-menu-command.js +128 -0
- package/dist/discord/command/create-context-menu-command.js.map +1 -0
- package/dist/discord/command/errors.d.ts +19 -0
- package/dist/discord/command/errors.d.ts.map +1 -0
- package/dist/discord/command/errors.js +27 -0
- package/dist/discord/command/errors.js.map +1 -0
- package/dist/discord/command/guard.d.ts +30 -0
- package/dist/discord/command/guard.d.ts.map +1 -0
- package/dist/discord/command/guard.js +34 -0
- package/dist/discord/command/guard.js.map +1 -0
- package/dist/discord/command/localization.d.ts +9 -0
- package/dist/discord/command/localization.d.ts.map +1 -0
- package/dist/discord/command/localization.js +13 -0
- package/dist/discord/command/localization.js.map +1 -0
- package/dist/discord/command/options.d.ts +113 -0
- package/dist/discord/command/options.d.ts.map +1 -0
- package/dist/discord/command/options.js +509 -0
- package/dist/discord/command/options.js.map +1 -0
- package/dist/discord/command/permission-guard.d.ts +21 -0
- package/dist/discord/command/permission-guard.d.ts.map +1 -0
- package/dist/discord/command/permission-guard.js +69 -0
- package/dist/discord/command/permission-guard.js.map +1 -0
- package/dist/discord/command/router.d.ts +48 -0
- package/dist/discord/command/router.d.ts.map +1 -0
- package/dist/discord/command/router.js +118 -0
- package/dist/discord/command/router.js.map +1 -0
- package/dist/discord/command/types.d.ts +51 -0
- package/dist/discord/command/types.d.ts.map +1 -0
- package/dist/discord/command/types.js +3 -0
- package/dist/discord/command/types.js.map +1 -0
- package/dist/discord/components/component-router.d.ts +116 -0
- package/dist/discord/components/component-router.d.ts.map +1 -0
- package/dist/discord/components/component-router.js +123 -0
- package/dist/discord/components/component-router.js.map +1 -0
- package/dist/discord/components/interactive-message/index.d.ts +3 -0
- package/dist/discord/components/interactive-message/index.d.ts.map +1 -0
- package/dist/discord/components/interactive-message/index.js +11 -0
- package/dist/discord/components/interactive-message/index.js.map +1 -0
- package/dist/discord/components/interactive-message/interactive-message-collector.d.ts +195 -0
- package/dist/discord/components/interactive-message/interactive-message-collector.d.ts.map +1 -0
- package/dist/discord/components/interactive-message/interactive-message-collector.js +151 -0
- package/dist/discord/components/interactive-message/interactive-message-collector.js.map +1 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.d.ts +30 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.d.ts.map +1 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.js +44 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.js.map +1 -0
- package/dist/discord/components/paginator/index.d.ts +5 -0
- package/dist/discord/components/paginator/index.d.ts.map +1 -0
- package/dist/discord/components/paginator/index.js +15 -0
- package/dist/discord/components/paginator/index.js.map +1 -0
- package/dist/discord/components/paginator/mount-paginator.d.ts +25 -0
- package/dist/discord/components/paginator/mount-paginator.d.ts.map +1 -0
- package/dist/discord/components/paginator/mount-paginator.js +41 -0
- package/dist/discord/components/paginator/mount-paginator.js.map +1 -0
- package/dist/discord/components/paginator/page-state.d.ts +21 -0
- package/dist/discord/components/paginator/page-state.d.ts.map +1 -0
- package/dist/discord/components/paginator/page-state.js +44 -0
- package/dist/discord/components/paginator/page-state.js.map +1 -0
- package/dist/discord/components/paginator/paginator-buttons.d.ts +17 -0
- package/dist/discord/components/paginator/paginator-buttons.d.ts.map +1 -0
- package/dist/discord/components/paginator/paginator-buttons.js +45 -0
- package/dist/discord/components/paginator/paginator-buttons.js.map +1 -0
- package/dist/discord/components/paginator/paginator-view.d.ts +26 -0
- package/dist/discord/components/paginator/paginator-view.d.ts.map +1 -0
- package/dist/discord/components/paginator/paginator-view.js +28 -0
- package/dist/discord/components/paginator/paginator-view.js.map +1 -0
- package/dist/discord/components/persistent-paginator.d.ts +47 -0
- package/dist/discord/components/persistent-paginator.d.ts.map +1 -0
- package/dist/discord/components/persistent-paginator.js +137 -0
- package/dist/discord/components/persistent-paginator.js.map +1 -0
- package/dist/discord/components/settings-editor/index.d.ts +7 -0
- package/dist/discord/components/settings-editor/index.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/index.js +22 -0
- package/dist/discord/components/settings-editor/index.js.map +1 -0
- package/dist/discord/components/settings-editor/mount-settings-editor.d.ts +140 -0
- package/dist/discord/components/settings-editor/mount-settings-editor.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/mount-settings-editor.js +344 -0
- package/dist/discord/components/settings-editor/mount-settings-editor.js.map +1 -0
- package/dist/discord/components/settings-editor/navigation.d.ts +49 -0
- package/dist/discord/components/settings-editor/navigation.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/navigation.js +39 -0
- package/dist/discord/components/settings-editor/navigation.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts +56 -0
- package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-card.view.js +284 -0
- package/dist/discord/components/settings-editor/settings-editor-card.view.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts +69 -0
- package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-field-modal.js +353 -0
- package/dist/discord/components/settings-editor/settings-editor-field-modal.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.d.ts +216 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.js +66 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor.view.d.ts +303 -0
- package/dist/discord/components/settings-editor/settings-editor.view.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor.view.js +316 -0
- package/dist/discord/components/settings-editor/settings-editor.view.js.map +1 -0
- package/dist/discord/events/create-event.d.ts +5 -0
- package/dist/discord/events/create-event.d.ts.map +1 -0
- package/dist/discord/events/create-event.js +8 -0
- package/dist/discord/events/create-event.js.map +1 -0
- package/dist/discord/events/event-router.d.ts +16 -0
- package/dist/discord/events/event-router.d.ts.map +1 -0
- package/dist/discord/events/event-router.js +51 -0
- package/dist/discord/events/event-router.js.map +1 -0
- package/dist/discord/events/types.d.ts +16 -0
- package/dist/discord/events/types.d.ts.map +1 -0
- package/dist/discord/events/types.js +3 -0
- package/dist/discord/events/types.js.map +1 -0
- package/dist/discord/i18n.d.ts +58 -0
- package/dist/discord/i18n.d.ts.map +1 -0
- package/dist/discord/i18n.js +173 -0
- package/dist/discord/i18n.js.map +1 -0
- package/dist/discord/interaction/button.d.ts +39 -0
- package/dist/discord/interaction/button.d.ts.map +1 -0
- package/dist/discord/interaction/button.js +36 -0
- package/dist/discord/interaction/button.js.map +1 -0
- package/dist/discord/interaction/interaction-router.d.ts +21 -0
- package/dist/discord/interaction/interaction-router.d.ts.map +1 -0
- package/dist/discord/interaction/interaction-router.js +24 -0
- package/dist/discord/interaction/interaction-router.js.map +1 -0
- package/dist/discord/interaction/modal.d.ts +171 -0
- package/dist/discord/interaction/modal.d.ts.map +1 -0
- package/dist/discord/interaction/modal.js +264 -0
- package/dist/discord/interaction/modal.js.map +1 -0
- package/dist/discord/interaction/select-menu.d.ts +104 -0
- package/dist/discord/interaction/select-menu.d.ts.map +1 -0
- package/dist/discord/interaction/select-menu.js +126 -0
- package/dist/discord/interaction/select-menu.js.map +1 -0
- package/dist/discord/permissions.d.ts +32 -0
- package/dist/discord/permissions.d.ts.map +1 -0
- package/dist/discord/permissions.js +40 -0
- package/dist/discord/permissions.js.map +1 -0
- package/dist/discord/placeholder-registry-errors.d.ts +10 -0
- package/dist/discord/placeholder-registry-errors.d.ts.map +1 -0
- package/dist/discord/placeholder-registry-errors.js +18 -0
- package/dist/discord/placeholder-registry-errors.js.map +1 -0
- package/dist/discord/placeholder-registry.d.ts +53 -0
- package/dist/discord/placeholder-registry.d.ts.map +1 -0
- package/dist/discord/placeholder-registry.js +52 -0
- package/dist/discord/placeholder-registry.js.map +1 -0
- package/dist/discord/presenter.d.ts +26 -0
- package/dist/discord/presenter.d.ts.map +1 -0
- package/dist/discord/presenter.js +3 -0
- package/dist/discord/presenter.js.map +1 -0
- package/dist/discord/ui/card.d.ts +114 -0
- package/dist/discord/ui/card.d.ts.map +1 -0
- package/dist/discord/ui/card.js +162 -0
- package/dist/discord/ui/card.js.map +1 -0
- package/dist/discord/ui/color-input-errors.d.ts +12 -0
- package/dist/discord/ui/color-input-errors.d.ts.map +1 -0
- package/dist/discord/ui/color-input-errors.js +21 -0
- package/dist/discord/ui/color-input-errors.js.map +1 -0
- package/dist/discord/ui/color-input.d.ts +37 -0
- package/dist/discord/ui/color-input.d.ts.map +1 -0
- package/dist/discord/ui/color-input.js +166 -0
- package/dist/discord/ui/color-input.js.map +1 -0
- package/dist/discord/ui/colors.d.ts +44 -0
- package/dist/discord/ui/colors.d.ts.map +1 -0
- package/dist/discord/ui/colors.js +45 -0
- package/dist/discord/ui/colors.js.map +1 -0
- package/dist/discord/ui/embed.d.ts +16 -0
- package/dist/discord/ui/embed.d.ts.map +1 -0
- package/dist/discord/ui/embed.js +53 -0
- package/dist/discord/ui/embed.js.map +1 -0
- package/dist/discord/ui/message-attachments.d.ts +40 -0
- package/dist/discord/ui/message-attachments.d.ts.map +1 -0
- package/dist/discord/ui/message-attachments.js +27 -0
- package/dist/discord/ui/message-attachments.js.map +1 -0
- package/dist/errors/business-error.d.ts +41 -0
- package/dist/errors/business-error.d.ts.map +1 -0
- package/dist/errors/business-error.js +39 -0
- package/dist/errors/business-error.js.map +1 -0
- package/dist/errors/describe-error.d.ts +29 -0
- package/dist/errors/describe-error.d.ts.map +1 -0
- package/dist/errors/describe-error.js +49 -0
- package/dist/errors/describe-error.js.map +1 -0
- package/dist/errors/incident-ref.d.ts +7 -0
- package/dist/errors/incident-ref.d.ts.map +1 -0
- package/dist/errors/incident-ref.js +13 -0
- package/dist/errors/incident-ref.js.map +1 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +15 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/i18n/business-message.d.ts +14 -0
- package/dist/i18n/business-message.d.ts.map +1 -0
- package/dist/i18n/business-message.js +19 -0
- package/dist/i18n/business-message.js.map +1 -0
- package/dist/i18n/catalog.d.ts +27 -0
- package/dist/i18n/catalog.d.ts.map +1 -0
- package/dist/i18n/catalog.js +25 -0
- package/dist/i18n/catalog.js.map +1 -0
- package/dist/i18n/errors.d.ts +10 -0
- package/dist/i18n/errors.d.ts.map +1 -0
- package/dist/i18n/errors.js +18 -0
- package/dist/i18n/errors.js.map +1 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +16 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/locale.d.ts +22 -0
- package/dist/i18n/locale.d.ts.map +1 -0
- package/dist/i18n/locale.js +40 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/i18n/translator.d.ts +35 -0
- package/dist/i18n/translator.d.ts.map +1 -0
- package/dist/i18n/translator.js +40 -0
- package/dist/i18n/translator.js.map +1 -0
- package/dist/logger.d.ts +28 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +3 -0
- package/dist/logger.js.map +1 -0
- package/dist/module/module.d.ts +42 -0
- package/dist/module/module.d.ts.map +1 -0
- package/dist/module/module.js +3 -0
- package/dist/module/module.js.map +1 -0
- package/dist/persistence/database.d.ts +24 -0
- package/dist/persistence/database.d.ts.map +1 -0
- package/dist/persistence/database.js +3 -0
- package/dist/persistence/database.js.map +1 -0
- package/dist/resilience/retry.d.ts +36 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +78 -0
- package/dist/resilience/retry.js.map +1 -0
- package/dist/scheduler/errors.d.ts +25 -0
- package/dist/scheduler/errors.d.ts.map +1 -0
- package/dist/scheduler/errors.js +45 -0
- package/dist/scheduler/errors.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +68 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -0
- package/dist/scheduler/scheduler.js +162 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/package.json +89 -0
- package/src/authz/authorizer.ts +76 -0
- package/src/authz/require-level-guard.ts +42 -0
- package/src/clock.ts +18 -0
- package/src/concurrency/keyed-queue.ts +50 -0
- package/src/concurrency/run-with-concurrency.ts +44 -0
- package/src/config/env.ts +58 -0
- package/src/config/errors.ts +21 -0
- package/src/datetime.ts +221 -0
- package/src/discord/api-errors.ts +91 -0
- package/src/discord/channel-exporter.ts +48 -0
- package/src/discord/command/autocomplete-dispatcher.ts +54 -0
- package/src/discord/command/autocomplete.ts +76 -0
- package/src/discord/command/context.ts +85 -0
- package/src/discord/command/cooldown-guard.ts +81 -0
- package/src/discord/command/create-command.ts +431 -0
- package/src/discord/command/create-context-menu-command.ts +227 -0
- package/src/discord/command/errors.ts +22 -0
- package/src/discord/command/guard.ts +54 -0
- package/src/discord/command/localization.ts +11 -0
- package/src/discord/command/options.ts +847 -0
- package/src/discord/command/permission-guard.ts +74 -0
- package/src/discord/command/router.ts +142 -0
- package/src/discord/command/types.ts +52 -0
- package/src/discord/components/component-router.ts +213 -0
- package/src/discord/components/interactive-message/index.ts +18 -0
- package/src/discord/components/interactive-message/interactive-message-collector.ts +374 -0
- package/src/discord/components/interactive-message/mount-interactive-message.ts +71 -0
- package/src/discord/components/paginator/index.ts +17 -0
- package/src/discord/components/paginator/mount-paginator.ts +69 -0
- package/src/discord/components/paginator/page-state.ts +45 -0
- package/src/discord/components/paginator/paginator-buttons.ts +60 -0
- package/src/discord/components/paginator/paginator-view.ts +61 -0
- package/src/discord/components/persistent-paginator.ts +197 -0
- package/src/discord/components/settings-editor/index.ts +41 -0
- package/src/discord/components/settings-editor/mount-settings-editor.ts +646 -0
- package/src/discord/components/settings-editor/navigation.ts +67 -0
- package/src/discord/components/settings-editor/settings-editor-card.view.ts +344 -0
- package/src/discord/components/settings-editor/settings-editor-field-modal.ts +538 -0
- package/src/discord/components/settings-editor/settings-editor-fields.ts +290 -0
- package/src/discord/components/settings-editor/settings-editor.view.ts +654 -0
- package/src/discord/events/create-event.ts +7 -0
- package/src/discord/events/event-router.ts +53 -0
- package/src/discord/events/types.ts +17 -0
- package/src/discord/i18n.ts +173 -0
- package/src/discord/interaction/button.ts +75 -0
- package/src/discord/interaction/interaction-router.ts +28 -0
- package/src/discord/interaction/modal.ts +505 -0
- package/src/discord/interaction/select-menu.ts +246 -0
- package/src/discord/permissions.ts +57 -0
- package/src/discord/placeholder-registry-errors.ts +11 -0
- package/src/discord/placeholder-registry.ts +77 -0
- package/src/discord/presenter.ts +26 -0
- package/src/discord/ui/card.ts +303 -0
- package/src/discord/ui/color-input-errors.ts +17 -0
- package/src/discord/ui/color-input.ts +179 -0
- package/src/discord/ui/colors.ts +58 -0
- package/src/discord/ui/embed.ts +64 -0
- package/src/discord/ui/message-attachments.ts +57 -0
- package/src/errors/business-error.ts +49 -0
- package/src/errors/describe-error.ts +73 -0
- package/src/errors/incident-ref.ts +10 -0
- package/src/errors/index.ts +12 -0
- package/src/i18n/business-message.ts +23 -0
- package/src/i18n/catalog.ts +41 -0
- package/src/i18n/errors.ts +11 -0
- package/src/i18n/index.ts +11 -0
- package/src/i18n/locale.ts +41 -0
- package/src/i18n/translator.ts +78 -0
- package/src/logger.ts +28 -0
- package/src/module/module.ts +42 -0
- package/src/persistence/database.ts +23 -0
- package/src/resilience/retry.ts +102 -0
- package/src/scheduler/errors.ts +40 -0
- package/src/scheduler/scheduler.ts +248 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApplicationCommandOptionAllowedChannelTypes,
|
|
3
|
+
Attachment,
|
|
4
|
+
AutocompleteInteraction,
|
|
5
|
+
ChannelType,
|
|
6
|
+
ChatInputCommandInteraction,
|
|
7
|
+
GuildBasedChannel,
|
|
8
|
+
LocalizationMap,
|
|
9
|
+
Role,
|
|
10
|
+
SlashCommandAttachmentOption,
|
|
11
|
+
SlashCommandBooleanOption,
|
|
12
|
+
SlashCommandChannelOption,
|
|
13
|
+
SlashCommandIntegerOption,
|
|
14
|
+
SlashCommandMentionableOption,
|
|
15
|
+
SlashCommandNumberOption,
|
|
16
|
+
SlashCommandRoleOption,
|
|
17
|
+
SlashCommandStringOption,
|
|
18
|
+
SlashCommandUserOption,
|
|
19
|
+
User,
|
|
20
|
+
} from "discord.js";
|
|
21
|
+
import { isValidWallClock } from "@/datetime";
|
|
22
|
+
import { OptionValidationError } from "@/discord/command/errors";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Translated name/description for a single option, keyed by Discord locale
|
|
26
|
+
* (e.g. `{ nameLocalizations: { "en-US": "channel", "en-GB": "channel" } }`).
|
|
27
|
+
* Every `create*Option` factory accepts this so an option's picker label can be
|
|
28
|
+
* localised the same way a command/subcommand name already can.
|
|
29
|
+
*/
|
|
30
|
+
export interface OptionLocalizations {
|
|
31
|
+
nameLocalizations?: LocalizationMap;
|
|
32
|
+
descriptionLocalizations?: LocalizationMap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Applies the localization maps to a builder, if any were provided. */
|
|
36
|
+
function applyOptionLocalizations(
|
|
37
|
+
option: {
|
|
38
|
+
setNameLocalizations(map: LocalizationMap): unknown;
|
|
39
|
+
setDescriptionLocalizations(map: LocalizationMap): unknown;
|
|
40
|
+
},
|
|
41
|
+
localizations: OptionLocalizations,
|
|
42
|
+
): void {
|
|
43
|
+
if (localizations.nameLocalizations) {
|
|
44
|
+
option.setNameLocalizations(localizations.nameLocalizations);
|
|
45
|
+
}
|
|
46
|
+
if (localizations.descriptionLocalizations) {
|
|
47
|
+
option.setDescriptionLocalizations(localizations.descriptionLocalizations);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The subset of a discord.js builder an option needs to register itself.
|
|
53
|
+
* Both `SlashCommandBuilder` (flat commands) and `SlashCommandSubcommandBuilder`
|
|
54
|
+
* satisfy this structurally, so the same option works on either.
|
|
55
|
+
*/
|
|
56
|
+
export interface OptionContainer {
|
|
57
|
+
addStringOption(input: (option: SlashCommandStringOption) => SlashCommandStringOption): unknown;
|
|
58
|
+
addUserOption(input: (option: SlashCommandUserOption) => SlashCommandUserOption): unknown;
|
|
59
|
+
addBooleanOption(
|
|
60
|
+
input: (option: SlashCommandBooleanOption) => SlashCommandBooleanOption,
|
|
61
|
+
): unknown;
|
|
62
|
+
addIntegerOption(
|
|
63
|
+
input: (option: SlashCommandIntegerOption) => SlashCommandIntegerOption,
|
|
64
|
+
): unknown;
|
|
65
|
+
addNumberOption(input: (option: SlashCommandNumberOption) => SlashCommandNumberOption): unknown;
|
|
66
|
+
addChannelOption(
|
|
67
|
+
input: (option: SlashCommandChannelOption) => SlashCommandChannelOption,
|
|
68
|
+
): unknown;
|
|
69
|
+
addRoleOption(input: (option: SlashCommandRoleOption) => SlashCommandRoleOption): unknown;
|
|
70
|
+
addMentionableOption(
|
|
71
|
+
input: (option: SlashCommandMentionableOption) => SlashCommandMentionableOption,
|
|
72
|
+
): unknown;
|
|
73
|
+
addAttachmentOption(
|
|
74
|
+
input: (option: SlashCommandAttachmentOption) => SlashCommandAttachmentOption,
|
|
75
|
+
): unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A single suggestion returned to Discord while the user types. */
|
|
79
|
+
export interface Choice {
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly value: string | number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Computes the suggestions for a focused option. Sync resolvers are allowed so a
|
|
86
|
+
* static list does not have to pay for a promise.
|
|
87
|
+
*/
|
|
88
|
+
export type AutocompleteResolver = (
|
|
89
|
+
interaction: AutocompleteInteraction,
|
|
90
|
+
) => Promise<readonly Choice[]> | readonly Choice[];
|
|
91
|
+
|
|
92
|
+
export interface Option<TValue, TRequired extends boolean> {
|
|
93
|
+
readonly description: string;
|
|
94
|
+
readonly isRequired: TRequired;
|
|
95
|
+
/** Present only on the option types Discord lets us autocomplete. */
|
|
96
|
+
readonly autocomplete?: AutocompleteResolver;
|
|
97
|
+
required(): Option<TValue, true>;
|
|
98
|
+
choices<const C extends readonly TValue[]>(values: C): Option<C[number], TRequired>;
|
|
99
|
+
apply(container: OptionContainer, name: string): void;
|
|
100
|
+
read(
|
|
101
|
+
interaction: ChatInputCommandInteraction,
|
|
102
|
+
name: string,
|
|
103
|
+
): TRequired extends true ? TValue : TValue | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The option types Discord accepts `autocomplete` on (string, integer, number).
|
|
108
|
+
* `required()` is narrowed so `.withAutocomplete()` stays reachable after it,
|
|
109
|
+
* while `choices()` deliberately falls back to a plain `Option`: Discord forbids
|
|
110
|
+
* combining both, so the illegal chain is unrepresentable rather than merely
|
|
111
|
+
* rejected at registration.
|
|
112
|
+
*/
|
|
113
|
+
export interface AutocompletableOption<TValue, TRequired extends boolean>
|
|
114
|
+
extends Option<TValue, TRequired> {
|
|
115
|
+
required(): AutocompletableOption<TValue, true>;
|
|
116
|
+
withAutocomplete(resolver: AutocompleteResolver): AutocompletableOption<TValue, TRequired>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// `Values<O>` only resolves required-ness correctly when `O` is inferred from an
|
|
120
|
+
// options literal. Annotating a variable as `Options` erases the `true`/`false`
|
|
121
|
+
// literals, so always let inference flow (e.g. through `createSubCommand()`), never annotate `: Options`.
|
|
122
|
+
export type Options = Record<string, Option<unknown, boolean>>;
|
|
123
|
+
|
|
124
|
+
export type Values<O extends Options> = {
|
|
125
|
+
[K in keyof O]: O[K] extends Option<infer V, infer R> ? (R extends true ? V : V | null) : never;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const CHOICES_WITH_AUTOCOMPLETE_MESSAGE =
|
|
129
|
+
"An option cannot declare both `choices` and `autocomplete`; Discord rejects the command.";
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Discord refuses a command whose option carries both a static choice list and
|
|
133
|
+
* an autocomplete resolver, so fail at registration rather than at deploy time.
|
|
134
|
+
*/
|
|
135
|
+
function assertChoicesAndAutocompleteExclusive(
|
|
136
|
+
hasChoices: boolean,
|
|
137
|
+
resolver: AutocompleteResolver | undefined,
|
|
138
|
+
): void {
|
|
139
|
+
if (hasChoices && resolver !== undefined) {
|
|
140
|
+
throw new OptionValidationError(CHOICES_WITH_AUTOCOMPLETE_MESSAGE);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Length limits for a string option, mirroring the numeric bounds below. */
|
|
145
|
+
interface StringLengthBounds {
|
|
146
|
+
minLength?: number;
|
|
147
|
+
maxLength?: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
class StringOption<TValue extends string, TRequired extends boolean>
|
|
151
|
+
implements AutocompletableOption<TValue, TRequired>
|
|
152
|
+
{
|
|
153
|
+
constructor(
|
|
154
|
+
readonly description: string,
|
|
155
|
+
readonly isRequired: TRequired,
|
|
156
|
+
private readonly choiceValues: readonly TValue[] | null,
|
|
157
|
+
private readonly validator: ((value: string) => void) | null,
|
|
158
|
+
private readonly bounds: StringLengthBounds,
|
|
159
|
+
readonly autocomplete?: AutocompleteResolver,
|
|
160
|
+
private readonly localizations: OptionLocalizations = {},
|
|
161
|
+
) {}
|
|
162
|
+
|
|
163
|
+
required(): AutocompletableOption<TValue, true> {
|
|
164
|
+
return new StringOption<TValue, true>(
|
|
165
|
+
this.description,
|
|
166
|
+
true,
|
|
167
|
+
this.choiceValues,
|
|
168
|
+
this.validator,
|
|
169
|
+
this.bounds,
|
|
170
|
+
this.autocomplete,
|
|
171
|
+
this.localizations,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
choices<const C extends readonly TValue[]>(values: C): Option<C[number], TRequired> {
|
|
176
|
+
return new StringOption<C[number] & string, TRequired>(
|
|
177
|
+
this.description,
|
|
178
|
+
this.isRequired,
|
|
179
|
+
values as readonly (C[number] & string)[],
|
|
180
|
+
this.validator,
|
|
181
|
+
this.bounds,
|
|
182
|
+
this.autocomplete,
|
|
183
|
+
this.localizations,
|
|
184
|
+
) as unknown as Option<C[number], TRequired>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
withAutocomplete(resolver: AutocompleteResolver): AutocompletableOption<TValue, TRequired> {
|
|
188
|
+
return new StringOption<TValue, TRequired>(
|
|
189
|
+
this.description,
|
|
190
|
+
this.isRequired,
|
|
191
|
+
this.choiceValues,
|
|
192
|
+
this.validator,
|
|
193
|
+
this.bounds,
|
|
194
|
+
resolver,
|
|
195
|
+
this.localizations,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
apply(container: OptionContainer, name: string): void {
|
|
200
|
+
assertChoicesAndAutocompleteExclusive(this.choiceValues !== null, this.autocomplete);
|
|
201
|
+
container.addStringOption((option) => {
|
|
202
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
203
|
+
applyOptionLocalizations(option, this.localizations);
|
|
204
|
+
if (this.choiceValues) {
|
|
205
|
+
option.addChoices(...this.choiceValues.map((value) => ({ name: value, value })));
|
|
206
|
+
}
|
|
207
|
+
if (this.bounds.minLength !== undefined) {
|
|
208
|
+
option.setMinLength(this.bounds.minLength);
|
|
209
|
+
}
|
|
210
|
+
if (this.bounds.maxLength !== undefined) {
|
|
211
|
+
option.setMaxLength(this.bounds.maxLength);
|
|
212
|
+
}
|
|
213
|
+
if (this.autocomplete !== undefined) {
|
|
214
|
+
option.setAutocomplete(true);
|
|
215
|
+
}
|
|
216
|
+
return option;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
read(
|
|
221
|
+
interaction: ChatInputCommandInteraction,
|
|
222
|
+
name: string,
|
|
223
|
+
): TRequired extends true ? TValue : TValue | null {
|
|
224
|
+
const raw = interaction.options.getString(name, this.isRequired as boolean);
|
|
225
|
+
if (raw !== null && this.validator) {
|
|
226
|
+
this.validator(raw);
|
|
227
|
+
}
|
|
228
|
+
return raw as TRequired extends true ? TValue : TValue | null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function createStringOption(
|
|
233
|
+
description: string,
|
|
234
|
+
bounds: StringLengthBounds = {},
|
|
235
|
+
localizations: OptionLocalizations = {},
|
|
236
|
+
): AutocompletableOption<string, false> {
|
|
237
|
+
return new StringOption<string, false>(
|
|
238
|
+
description,
|
|
239
|
+
false,
|
|
240
|
+
null,
|
|
241
|
+
null,
|
|
242
|
+
bounds,
|
|
243
|
+
undefined,
|
|
244
|
+
localizations,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
class UserOption<TRequired extends boolean> implements Option<User, TRequired> {
|
|
249
|
+
constructor(
|
|
250
|
+
readonly description: string,
|
|
251
|
+
readonly isRequired: TRequired,
|
|
252
|
+
private readonly localizations: OptionLocalizations = {},
|
|
253
|
+
) {}
|
|
254
|
+
|
|
255
|
+
required(): Option<User, true> {
|
|
256
|
+
return new UserOption<true>(this.description, true, this.localizations);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
choices<const C extends readonly User[]>(_values: C): Option<C[number], TRequired> {
|
|
260
|
+
throw new OptionValidationError("User options do not support choices.");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
apply(container: OptionContainer, name: string): void {
|
|
264
|
+
container.addUserOption((option) => {
|
|
265
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
266
|
+
applyOptionLocalizations(option, this.localizations);
|
|
267
|
+
return option;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
read(
|
|
272
|
+
interaction: ChatInputCommandInteraction,
|
|
273
|
+
name: string,
|
|
274
|
+
): TRequired extends true ? User : User | null {
|
|
275
|
+
return interaction.options.getUser(name, this.isRequired as boolean) as TRequired extends true
|
|
276
|
+
? User
|
|
277
|
+
: User | null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function createUserOption(
|
|
282
|
+
description: string,
|
|
283
|
+
localizations: OptionLocalizations = {},
|
|
284
|
+
): Option<User, false> {
|
|
285
|
+
return new UserOption<false>(description, false, localizations);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
class BooleanOption<TRequired extends boolean> implements Option<boolean, TRequired> {
|
|
289
|
+
constructor(
|
|
290
|
+
readonly description: string,
|
|
291
|
+
readonly isRequired: TRequired,
|
|
292
|
+
private readonly localizations: OptionLocalizations = {},
|
|
293
|
+
) {}
|
|
294
|
+
|
|
295
|
+
required(): Option<boolean, true> {
|
|
296
|
+
return new BooleanOption<true>(this.description, true, this.localizations);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
choices<const C extends readonly boolean[]>(_values: C): Option<C[number], TRequired> {
|
|
300
|
+
throw new OptionValidationError("Boolean options do not support choices.");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
apply(container: OptionContainer, name: string): void {
|
|
304
|
+
container.addBooleanOption((option) => {
|
|
305
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
306
|
+
applyOptionLocalizations(option, this.localizations);
|
|
307
|
+
return option;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
read(
|
|
312
|
+
interaction: ChatInputCommandInteraction,
|
|
313
|
+
name: string,
|
|
314
|
+
): TRequired extends true ? boolean : boolean | null {
|
|
315
|
+
return interaction.options.getBoolean(
|
|
316
|
+
name,
|
|
317
|
+
this.isRequired as boolean,
|
|
318
|
+
) as TRequired extends true ? boolean : boolean | null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function createBooleanOption(
|
|
323
|
+
description: string,
|
|
324
|
+
localizations: OptionLocalizations = {},
|
|
325
|
+
): Option<boolean, false> {
|
|
326
|
+
return new BooleanOption<false>(description, false, localizations);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface IntegerBounds {
|
|
330
|
+
min?: number;
|
|
331
|
+
max?: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
class IntegerOption<TRequired extends boolean> implements AutocompletableOption<number, TRequired> {
|
|
335
|
+
constructor(
|
|
336
|
+
readonly description: string,
|
|
337
|
+
readonly isRequired: TRequired,
|
|
338
|
+
private readonly bounds: IntegerBounds,
|
|
339
|
+
readonly autocomplete?: AutocompleteResolver,
|
|
340
|
+
private readonly localizations: OptionLocalizations = {},
|
|
341
|
+
) {}
|
|
342
|
+
|
|
343
|
+
required(): AutocompletableOption<number, true> {
|
|
344
|
+
return new IntegerOption<true>(
|
|
345
|
+
this.description,
|
|
346
|
+
true,
|
|
347
|
+
this.bounds,
|
|
348
|
+
this.autocomplete,
|
|
349
|
+
this.localizations,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
choices<const C extends readonly number[]>(_values: C): Option<C[number], TRequired> {
|
|
354
|
+
throw new OptionValidationError("Integer options do not support choices.");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
withAutocomplete(resolver: AutocompleteResolver): AutocompletableOption<number, TRequired> {
|
|
358
|
+
return new IntegerOption<TRequired>(
|
|
359
|
+
this.description,
|
|
360
|
+
this.isRequired,
|
|
361
|
+
this.bounds,
|
|
362
|
+
resolver,
|
|
363
|
+
this.localizations,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
apply(container: OptionContainer, name: string): void {
|
|
368
|
+
container.addIntegerOption((option) => {
|
|
369
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
370
|
+
applyOptionLocalizations(option, this.localizations);
|
|
371
|
+
if (this.bounds.min !== undefined) {
|
|
372
|
+
option.setMinValue(this.bounds.min);
|
|
373
|
+
}
|
|
374
|
+
if (this.bounds.max !== undefined) {
|
|
375
|
+
option.setMaxValue(this.bounds.max);
|
|
376
|
+
}
|
|
377
|
+
if (this.autocomplete !== undefined) {
|
|
378
|
+
option.setAutocomplete(true);
|
|
379
|
+
}
|
|
380
|
+
return option;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
read(
|
|
385
|
+
interaction: ChatInputCommandInteraction,
|
|
386
|
+
name: string,
|
|
387
|
+
): TRequired extends true ? number : number | null {
|
|
388
|
+
return interaction.options.getInteger(
|
|
389
|
+
name,
|
|
390
|
+
this.isRequired as boolean,
|
|
391
|
+
) as TRequired extends true ? number : number | null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function createIntegerOption(
|
|
396
|
+
description: string,
|
|
397
|
+
bounds: IntegerBounds = {},
|
|
398
|
+
localizations: OptionLocalizations = {},
|
|
399
|
+
): AutocompletableOption<number, false> {
|
|
400
|
+
return new IntegerOption<false>(description, false, bounds, undefined, localizations);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
class NumberOption<TRequired extends boolean> implements AutocompletableOption<number, TRequired> {
|
|
404
|
+
constructor(
|
|
405
|
+
readonly description: string,
|
|
406
|
+
readonly isRequired: TRequired,
|
|
407
|
+
private readonly bounds: IntegerBounds,
|
|
408
|
+
readonly autocomplete?: AutocompleteResolver,
|
|
409
|
+
private readonly localizations: OptionLocalizations = {},
|
|
410
|
+
) {}
|
|
411
|
+
|
|
412
|
+
required(): AutocompletableOption<number, true> {
|
|
413
|
+
return new NumberOption<true>(
|
|
414
|
+
this.description,
|
|
415
|
+
true,
|
|
416
|
+
this.bounds,
|
|
417
|
+
this.autocomplete,
|
|
418
|
+
this.localizations,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
choices<const C extends readonly number[]>(_values: C): Option<C[number], TRequired> {
|
|
423
|
+
throw new OptionValidationError("Number options do not support choices.");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
withAutocomplete(resolver: AutocompleteResolver): AutocompletableOption<number, TRequired> {
|
|
427
|
+
return new NumberOption<TRequired>(
|
|
428
|
+
this.description,
|
|
429
|
+
this.isRequired,
|
|
430
|
+
this.bounds,
|
|
431
|
+
resolver,
|
|
432
|
+
this.localizations,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
apply(container: OptionContainer, name: string): void {
|
|
437
|
+
container.addNumberOption((option) => {
|
|
438
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
439
|
+
applyOptionLocalizations(option, this.localizations);
|
|
440
|
+
if (this.bounds.min !== undefined) {
|
|
441
|
+
option.setMinValue(this.bounds.min);
|
|
442
|
+
}
|
|
443
|
+
if (this.bounds.max !== undefined) {
|
|
444
|
+
option.setMaxValue(this.bounds.max);
|
|
445
|
+
}
|
|
446
|
+
if (this.autocomplete !== undefined) {
|
|
447
|
+
option.setAutocomplete(true);
|
|
448
|
+
}
|
|
449
|
+
return option;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
read(
|
|
454
|
+
interaction: ChatInputCommandInteraction,
|
|
455
|
+
name: string,
|
|
456
|
+
): TRequired extends true ? number : number | null {
|
|
457
|
+
return interaction.options.getNumber(name, this.isRequired as boolean) as TRequired extends true
|
|
458
|
+
? number
|
|
459
|
+
: number | null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function createNumberOption(
|
|
464
|
+
description: string,
|
|
465
|
+
bounds: IntegerBounds = {},
|
|
466
|
+
localizations: OptionLocalizations = {},
|
|
467
|
+
): AutocompletableOption<number, false> {
|
|
468
|
+
return new NumberOption<false>(description, false, bounds, undefined, localizations);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const INVALID_DATETIME_MESSAGE =
|
|
472
|
+
"Invalid date format. Use `YYYY-MM-DD HH:MM` (e.g. `2025-03-15 14:00`).";
|
|
473
|
+
|
|
474
|
+
function assertValidDate(value: string, message: string): void {
|
|
475
|
+
if (Number.isNaN(Date.parse(value))) {
|
|
476
|
+
throw new OptionValidationError(message);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
class DateTimeOption<TRequired extends boolean> implements Option<Date, TRequired> {
|
|
481
|
+
constructor(
|
|
482
|
+
readonly description: string,
|
|
483
|
+
readonly isRequired: TRequired,
|
|
484
|
+
private readonly localizations: OptionLocalizations = {},
|
|
485
|
+
) {}
|
|
486
|
+
|
|
487
|
+
required(): Option<Date, true> {
|
|
488
|
+
return new DateTimeOption<true>(this.description, true, this.localizations);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
choices<const C extends readonly Date[]>(_values: C): Option<C[number], TRequired> {
|
|
492
|
+
throw new OptionValidationError("Date options do not support choices.");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
apply(container: OptionContainer, name: string): void {
|
|
496
|
+
container.addStringOption((option) => {
|
|
497
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
498
|
+
applyOptionLocalizations(option, this.localizations);
|
|
499
|
+
return option;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
read(
|
|
504
|
+
interaction: ChatInputCommandInteraction,
|
|
505
|
+
name: string,
|
|
506
|
+
): TRequired extends true ? Date : Date | null {
|
|
507
|
+
const raw = interaction.options.getString(name, this.isRequired as boolean);
|
|
508
|
+
if (raw === null) {
|
|
509
|
+
return null as TRequired extends true ? Date : Date | null;
|
|
510
|
+
}
|
|
511
|
+
assertValidDate(raw, INVALID_DATETIME_MESSAGE);
|
|
512
|
+
return new Date(raw.replace(" ", "T")) as TRequired extends true ? Date : Date | null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function createDateTimeOption(
|
|
517
|
+
description: string,
|
|
518
|
+
localizations: OptionLocalizations = {},
|
|
519
|
+
): Option<Date, false> {
|
|
520
|
+
return new DateTimeOption<false>(description, false, localizations);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const INVALID_WALL_CLOCK_MESSAGE =
|
|
524
|
+
"Invalid date format. Use `YYYY-MM-DD` or `YYYY-MM-DD HH:MM` (e.g. `2025-03-15 14:00`).";
|
|
525
|
+
const INVALID_DATE_ONLY_MESSAGE = "Invalid date format. Use `YYYY-MM-DD` (e.g. `2025-03-15`).";
|
|
526
|
+
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* A raw `YYYY-MM-DD[ HH:MM[:SS]]` string, validated but *not* anchored to any
|
|
530
|
+
* timezone here — the core has no notion of "the guild's timezone", so
|
|
531
|
+
* anchoring (`zonedWallClockToUtc`) is the caller's job, once it knows which
|
|
532
|
+
* IANA zone applies. This is what lets the same option be reused by any module
|
|
533
|
+
* that needs a wall-clock input.
|
|
534
|
+
*/
|
|
535
|
+
class WallClockOption<TRequired extends boolean> implements Option<string, TRequired> {
|
|
536
|
+
constructor(
|
|
537
|
+
readonly description: string,
|
|
538
|
+
readonly isRequired: TRequired,
|
|
539
|
+
private readonly localizations: OptionLocalizations = {},
|
|
540
|
+
) {}
|
|
541
|
+
|
|
542
|
+
required(): Option<string, true> {
|
|
543
|
+
return new WallClockOption<true>(this.description, true, this.localizations);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
choices<const C extends readonly string[]>(_values: C): Option<C[number], TRequired> {
|
|
547
|
+
throw new OptionValidationError("Wall-clock options do not support choices.");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
apply(container: OptionContainer, name: string): void {
|
|
551
|
+
container.addStringOption((option) => {
|
|
552
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
553
|
+
applyOptionLocalizations(option, this.localizations);
|
|
554
|
+
return option;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
read(
|
|
559
|
+
interaction: ChatInputCommandInteraction,
|
|
560
|
+
name: string,
|
|
561
|
+
): TRequired extends true ? string : string | null {
|
|
562
|
+
const raw = interaction.options.getString(name, this.isRequired as boolean);
|
|
563
|
+
if (raw === null) {
|
|
564
|
+
return null as TRequired extends true ? string : string | null;
|
|
565
|
+
}
|
|
566
|
+
if (!isValidWallClock(raw)) {
|
|
567
|
+
throw new OptionValidationError(INVALID_WALL_CLOCK_MESSAGE);
|
|
568
|
+
}
|
|
569
|
+
return raw as TRequired extends true ? string : string | null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* A `YYYY-MM-DD[ HH:MM[:SS]]` string option, validated against the wall-clock
|
|
575
|
+
* grammar but left unanchored (no `Date` conversion, no assumed timezone) — the
|
|
576
|
+
* caller anchors it with `zonedWallClockToUtc` once it knows the guild zone.
|
|
577
|
+
*/
|
|
578
|
+
export function createWallClockOption(
|
|
579
|
+
description: string,
|
|
580
|
+
localizations: OptionLocalizations = {},
|
|
581
|
+
): Option<string, false> {
|
|
582
|
+
return new WallClockOption<false>(description, false, localizations);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** A strict, date-only (`YYYY-MM-DD`) option — rejects any time component. */
|
|
586
|
+
class DateOption<TRequired extends boolean> implements Option<Date, TRequired> {
|
|
587
|
+
constructor(
|
|
588
|
+
readonly description: string,
|
|
589
|
+
readonly isRequired: TRequired,
|
|
590
|
+
private readonly localizations: OptionLocalizations = {},
|
|
591
|
+
) {}
|
|
592
|
+
|
|
593
|
+
required(): Option<Date, true> {
|
|
594
|
+
return new DateOption<true>(this.description, true, this.localizations);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
choices<const C extends readonly Date[]>(_values: C): Option<C[number], TRequired> {
|
|
598
|
+
throw new OptionValidationError("Date options do not support choices.");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
apply(container: OptionContainer, name: string): void {
|
|
602
|
+
container.addStringOption((option) => {
|
|
603
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
604
|
+
applyOptionLocalizations(option, this.localizations);
|
|
605
|
+
return option;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
read(
|
|
610
|
+
interaction: ChatInputCommandInteraction,
|
|
611
|
+
name: string,
|
|
612
|
+
): TRequired extends true ? Date : Date | null {
|
|
613
|
+
const raw = interaction.options.getString(name, this.isRequired as boolean);
|
|
614
|
+
if (raw === null) {
|
|
615
|
+
return null as TRequired extends true ? Date : Date | null;
|
|
616
|
+
}
|
|
617
|
+
// Strict `YYYY-MM-DD` with a real-calendar check (rejects `2026/03/15`,
|
|
618
|
+
// time-bearing input, and impossible dates like `2026-02-30`).
|
|
619
|
+
if (!DATE_ONLY_PATTERN.test(raw) || !isValidWallClock(raw)) {
|
|
620
|
+
throw new OptionValidationError(INVALID_DATE_ONLY_MESSAGE);
|
|
621
|
+
}
|
|
622
|
+
return new Date(raw) as TRequired extends true ? Date : Date | null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* A strict date-only (`YYYY-MM-DD`) option. The returned `Date` is UTC
|
|
628
|
+
* midnight of that calendar day — a caller that needs it anchored to a guild
|
|
629
|
+
* timezone (start/end of that guild-local day) uses `startOfGuildDay`/
|
|
630
|
+
* `endOfGuildDay` from `core/datetime`.
|
|
631
|
+
*/
|
|
632
|
+
export function createDateOption(
|
|
633
|
+
description: string,
|
|
634
|
+
localizations: OptionLocalizations = {},
|
|
635
|
+
): Option<Date, false> {
|
|
636
|
+
return new DateOption<false>(description, false, localizations);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** The channel types Discord accepts on a channel option (guild channels only). */
|
|
640
|
+
type AllowedChannelType = ApplicationCommandOptionAllowedChannelTypes;
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Narrows the guild channel union to the declared channel types, mirroring the
|
|
644
|
+
* `Extract<>` mapping of `CommandInteractionOptionResolver.getChannel`. The
|
|
645
|
+
* thread ternary is load-bearing: `PublicThreadChannel.type` is
|
|
646
|
+
* `PublicThread | AnnouncementThread`, so extracting on a single one of them
|
|
647
|
+
* would collapse to `never`.
|
|
648
|
+
*/
|
|
649
|
+
export type ChannelOptionValue<TType extends AllowedChannelType> = Extract<
|
|
650
|
+
GuildBasedChannel,
|
|
651
|
+
{
|
|
652
|
+
type: TType extends ChannelType.PublicThread | ChannelType.AnnouncementThread
|
|
653
|
+
? ChannelType.PublicThread | ChannelType.AnnouncementThread
|
|
654
|
+
: TType;
|
|
655
|
+
}
|
|
656
|
+
>;
|
|
657
|
+
|
|
658
|
+
class ChannelOption<TType extends AllowedChannelType, TRequired extends boolean>
|
|
659
|
+
implements Option<ChannelOptionValue<TType>, TRequired>
|
|
660
|
+
{
|
|
661
|
+
constructor(
|
|
662
|
+
readonly description: string,
|
|
663
|
+
readonly isRequired: TRequired,
|
|
664
|
+
private readonly channelTypes: readonly TType[] | undefined,
|
|
665
|
+
private readonly localizations: OptionLocalizations = {},
|
|
666
|
+
) {}
|
|
667
|
+
|
|
668
|
+
required(): Option<ChannelOptionValue<TType>, true> {
|
|
669
|
+
return new ChannelOption<TType, true>(
|
|
670
|
+
this.description,
|
|
671
|
+
true,
|
|
672
|
+
this.channelTypes,
|
|
673
|
+
this.localizations,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
choices<const C extends readonly ChannelOptionValue<TType>[]>(
|
|
678
|
+
_values: C,
|
|
679
|
+
): Option<C[number], TRequired> {
|
|
680
|
+
throw new OptionValidationError("Channel options do not support choices.");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
apply(container: OptionContainer, name: string): void {
|
|
684
|
+
container.addChannelOption((option) => {
|
|
685
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
686
|
+
applyOptionLocalizations(option, this.localizations);
|
|
687
|
+
if (this.channelTypes !== undefined) {
|
|
688
|
+
option.addChannelTypes(...this.channelTypes);
|
|
689
|
+
}
|
|
690
|
+
return option;
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
read(
|
|
695
|
+
interaction: ChatInputCommandInteraction,
|
|
696
|
+
name: string,
|
|
697
|
+
): TRequired extends true ? ChannelOptionValue<TType> : ChannelOptionValue<TType> | null {
|
|
698
|
+
// The declared types are re-sent at read time: `addChannelTypes` only drives
|
|
699
|
+
// the client-side picker, so Discord can still deliver another channel kind.
|
|
700
|
+
return interaction.options.getChannel(
|
|
701
|
+
name,
|
|
702
|
+
this.isRequired as boolean,
|
|
703
|
+
this.channelTypes,
|
|
704
|
+
) as TRequired extends true ? ChannelOptionValue<TType> : ChannelOptionValue<TType> | null;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function createChannelOption<const TType extends AllowedChannelType = AllowedChannelType>(
|
|
709
|
+
description: string,
|
|
710
|
+
channelTypes?: readonly TType[],
|
|
711
|
+
localizations: OptionLocalizations = {},
|
|
712
|
+
): Option<ChannelOptionValue<TType>, false> {
|
|
713
|
+
return new ChannelOption<TType, false>(description, false, channelTypes, localizations);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
class RoleOption<TRequired extends boolean> implements Option<Role, TRequired> {
|
|
717
|
+
constructor(
|
|
718
|
+
readonly description: string,
|
|
719
|
+
readonly isRequired: TRequired,
|
|
720
|
+
private readonly localizations: OptionLocalizations = {},
|
|
721
|
+
) {}
|
|
722
|
+
|
|
723
|
+
required(): Option<Role, true> {
|
|
724
|
+
return new RoleOption<true>(this.description, true, this.localizations);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
choices<const C extends readonly Role[]>(_values: C): Option<C[number], TRequired> {
|
|
728
|
+
throw new OptionValidationError("Role options do not support choices.");
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
apply(container: OptionContainer, name: string): void {
|
|
732
|
+
container.addRoleOption((option) => {
|
|
733
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
734
|
+
applyOptionLocalizations(option, this.localizations);
|
|
735
|
+
return option;
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
read(
|
|
740
|
+
interaction: ChatInputCommandInteraction,
|
|
741
|
+
name: string,
|
|
742
|
+
): TRequired extends true ? Role : Role | null {
|
|
743
|
+
return interaction.options.getRole(name, this.isRequired as boolean) as TRequired extends true
|
|
744
|
+
? Role
|
|
745
|
+
: Role | null;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function createRoleOption(
|
|
750
|
+
description: string,
|
|
751
|
+
localizations: OptionLocalizations = {},
|
|
752
|
+
): Option<Role, false> {
|
|
753
|
+
return new RoleOption<false>(description, false, localizations);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
class MentionableOption<TRequired extends boolean> implements Option<Role | User, TRequired> {
|
|
757
|
+
constructor(
|
|
758
|
+
readonly description: string,
|
|
759
|
+
readonly isRequired: TRequired,
|
|
760
|
+
private readonly localizations: OptionLocalizations = {},
|
|
761
|
+
) {}
|
|
762
|
+
|
|
763
|
+
required(): Option<Role | User, true> {
|
|
764
|
+
return new MentionableOption<true>(this.description, true, this.localizations);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
choices<const C extends readonly (Role | User)[]>(_values: C): Option<C[number], TRequired> {
|
|
768
|
+
throw new OptionValidationError("Mentionable options do not support choices.");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
apply(container: OptionContainer, name: string): void {
|
|
772
|
+
container.addMentionableOption((option) => {
|
|
773
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
774
|
+
applyOptionLocalizations(option, this.localizations);
|
|
775
|
+
return option;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
read(
|
|
780
|
+
interaction: ChatInputCommandInteraction,
|
|
781
|
+
name: string,
|
|
782
|
+
): TRequired extends true ? Role | User : Role | User | null {
|
|
783
|
+
return interaction.options.getMentionable(
|
|
784
|
+
name,
|
|
785
|
+
this.isRequired as boolean,
|
|
786
|
+
) as TRequired extends true ? Role | User : Role | User | null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function createMentionableOption(
|
|
791
|
+
description: string,
|
|
792
|
+
localizations: OptionLocalizations = {},
|
|
793
|
+
): Option<Role | User, false> {
|
|
794
|
+
return new MentionableOption<false>(description, false, localizations);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
class AttachmentOption<TRequired extends boolean> implements Option<Attachment, TRequired> {
|
|
798
|
+
constructor(
|
|
799
|
+
readonly description: string,
|
|
800
|
+
readonly isRequired: TRequired,
|
|
801
|
+
private readonly localizations: OptionLocalizations = {},
|
|
802
|
+
) {}
|
|
803
|
+
|
|
804
|
+
required(): Option<Attachment, true> {
|
|
805
|
+
return new AttachmentOption<true>(this.description, true, this.localizations);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
choices<const C extends readonly Attachment[]>(_values: C): Option<C[number], TRequired> {
|
|
809
|
+
throw new OptionValidationError("Attachment options do not support choices.");
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
apply(container: OptionContainer, name: string): void {
|
|
813
|
+
container.addAttachmentOption((option) => {
|
|
814
|
+
option.setName(name).setDescription(this.description).setRequired(this.isRequired);
|
|
815
|
+
applyOptionLocalizations(option, this.localizations);
|
|
816
|
+
return option;
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
read(
|
|
821
|
+
interaction: ChatInputCommandInteraction,
|
|
822
|
+
name: string,
|
|
823
|
+
): TRequired extends true ? Attachment : Attachment | null {
|
|
824
|
+
return interaction.options.getAttachment(
|
|
825
|
+
name,
|
|
826
|
+
this.isRequired as boolean,
|
|
827
|
+
) as TRequired extends true ? Attachment : Attachment | null;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export function createAttachmentOption(
|
|
832
|
+
description: string,
|
|
833
|
+
localizations: OptionLocalizations = {},
|
|
834
|
+
): Option<Attachment, false> {
|
|
835
|
+
return new AttachmentOption<false>(description, false, localizations);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
type AsRequired<O> = O extends Option<infer V, boolean> ? Option<V, true> : never;
|
|
839
|
+
|
|
840
|
+
/** Turn every option in a record into its required variant. */
|
|
841
|
+
export function allRequired<O extends Options>(fields: O): { [K in keyof O]: AsRequired<O[K]> } {
|
|
842
|
+
const out = {} as { [K in keyof O]: AsRequired<O[K]> };
|
|
843
|
+
for (const [key, option] of Object.entries(fields)) {
|
|
844
|
+
out[key as keyof O] = option.required() as AsRequired<O[keyof O]>;
|
|
845
|
+
}
|
|
846
|
+
return out;
|
|
847
|
+
}
|