@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,91 @@
|
|
|
1
|
+
import { DiscordAPIError, RESTJSONErrorCodes } from "discord.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discord answers a rate limit with an HTTP status, not a JSON error code, so
|
|
5
|
+
* this one cannot come from {@link RESTJSONErrorCodes}.
|
|
6
|
+
*/
|
|
7
|
+
const TOO_MANY_REQUESTS_STATUS = 429;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Codes meaning "the entity you asked about no longer exists". They are benign:
|
|
11
|
+
* a channel was deleted, a member left, a message was removed, an interaction
|
|
12
|
+
* token expired. The right reaction is to clean up local state, never to alert.
|
|
13
|
+
*/
|
|
14
|
+
const UNKNOWN_RESOURCE_CODES: ReadonlySet<number> = new Set([
|
|
15
|
+
RESTJSONErrorCodes.UnknownAccount,
|
|
16
|
+
RESTJSONErrorCodes.UnknownApplication,
|
|
17
|
+
RESTJSONErrorCodes.UnknownChannel,
|
|
18
|
+
RESTJSONErrorCodes.UnknownGuild,
|
|
19
|
+
RESTJSONErrorCodes.UnknownIntegration,
|
|
20
|
+
RESTJSONErrorCodes.UnknownInvite,
|
|
21
|
+
RESTJSONErrorCodes.UnknownMember,
|
|
22
|
+
RESTJSONErrorCodes.UnknownMessage,
|
|
23
|
+
RESTJSONErrorCodes.UnknownPermissionOverwrite,
|
|
24
|
+
RESTJSONErrorCodes.UnknownRole,
|
|
25
|
+
RESTJSONErrorCodes.UnknownToken,
|
|
26
|
+
RESTJSONErrorCodes.UnknownUser,
|
|
27
|
+
RESTJSONErrorCodes.UnknownEmoji,
|
|
28
|
+
RESTJSONErrorCodes.UnknownWebhook,
|
|
29
|
+
RESTJSONErrorCodes.UnknownBan,
|
|
30
|
+
RESTJSONErrorCodes.UnknownGuildTemplate,
|
|
31
|
+
RESTJSONErrorCodes.UnknownSticker,
|
|
32
|
+
RESTJSONErrorCodes.UnknownInteraction,
|
|
33
|
+
RESTJSONErrorCodes.UnknownApplicationCommand,
|
|
34
|
+
RESTJSONErrorCodes.UnknownVoiceState,
|
|
35
|
+
RESTJSONErrorCodes.UnknownStageInstance,
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** Codes meaning the bot is simply not allowed to do this — actionable by an admin. */
|
|
39
|
+
const MISSING_ACCESS_CODES: ReadonlySet<number> = new Set([
|
|
40
|
+
RESTJSONErrorCodes.MissingAccess,
|
|
41
|
+
RESTJSONErrorCodes.MissingPermissions,
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** Codes Discord uses for a per-resource throttle, alongside the HTTP 429 status. */
|
|
45
|
+
const RATE_LIMIT_CODES: ReadonlySet<number> = new Set([
|
|
46
|
+
RESTJSONErrorCodes.ChannelSendRateLimit,
|
|
47
|
+
RESTJSONErrorCodes.ServerSendRateLimit,
|
|
48
|
+
RESTJSONErrorCodes.ServiceResourceIsBeingRateLimited,
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Narrow an unknown rejection to a Discord REST error. Adapters catch `unknown`,
|
|
53
|
+
* so every predicate below starts here rather than trusting a `code` property
|
|
54
|
+
* that any object could carry.
|
|
55
|
+
*/
|
|
56
|
+
export function isDiscordAPIError(error: unknown): error is DiscordAPIError {
|
|
57
|
+
return error instanceof DiscordAPIError;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The targeted guild/channel/member/message/interaction is gone. Treat as a
|
|
62
|
+
* benign outcome: drop the stale reference instead of logging an incident.
|
|
63
|
+
*/
|
|
64
|
+
export function isUnknownResource(error: unknown): boolean {
|
|
65
|
+
if (!isDiscordAPIError(error)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return typeof error.code === "number" && UNKNOWN_RESOURCE_CODES.has(error.code);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The bot lacks a permission or access to the resource. Actionable by a server
|
|
73
|
+
* admin, so it deserves a clear message rather than a generic incident.
|
|
74
|
+
*/
|
|
75
|
+
export function isMissingAccess(error: unknown): boolean {
|
|
76
|
+
if (!isDiscordAPIError(error)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return typeof error.code === "number" && MISSING_ACCESS_CODES.has(error.code);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Discord is throttling us: back off and retry rather than surface a failure. */
|
|
83
|
+
export function isRateLimited(error: unknown): boolean {
|
|
84
|
+
if (!isDiscordAPIError(error)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (error.status === TOO_MANY_REQUESTS_STATUS) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return typeof error.code === "number" && RATE_LIMIT_CODES.has(error.code);
|
|
91
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for handing a channel to whatever export/archive pipeline is wired up at
|
|
3
|
+
* the composition root, before the channel is otherwise disposed of.
|
|
4
|
+
*
|
|
5
|
+
* This is the seam that lets one feature use another's capability without
|
|
6
|
+
* importing it: a module that closes channels can ask for the content to be
|
|
7
|
+
* archived first, while knowing nothing about who archives it — or whether
|
|
8
|
+
* anyone does. The composition root decides what implements this, exactly as it
|
|
9
|
+
* does for `Logger` and `DatabaseConnection`.
|
|
10
|
+
*/
|
|
11
|
+
export interface ChannelExportOptions {
|
|
12
|
+
/** A caller-chosen base name (no extension) for the produced archive file. */
|
|
13
|
+
readonly fileName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Where one export actually ended up.
|
|
18
|
+
*
|
|
19
|
+
* Reported rather than assumed, because a caller usually has to tell the person
|
|
20
|
+
* who triggered the export where to find the result — and because an
|
|
21
|
+
* implementation may have a preferred destination it could not reach. The two
|
|
22
|
+
* destinations are deliberately coarse: `discord`, meaning the archive was
|
|
23
|
+
* posted as a message attachment, and `cloud`, meaning it was uploaded
|
|
24
|
+
* somewhere and only a link came back.
|
|
25
|
+
*/
|
|
26
|
+
export interface ChannelExportOutcome {
|
|
27
|
+
readonly destinationUsed: "discord" | "cloud";
|
|
28
|
+
/** True when cloud delivery was configured, failed, and Discord took over. */
|
|
29
|
+
readonly fellBackFromCloud: boolean;
|
|
30
|
+
readonly cloudUrl?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ChannelExporter {
|
|
34
|
+
/**
|
|
35
|
+
* Export `channelId`'s content and deliver the result.
|
|
36
|
+
*
|
|
37
|
+
* `deliverToChannelId` is where a Discord-delivered archive is posted — also
|
|
38
|
+
* the fallback when cloud delivery is configured but fails. `guildId` is what
|
|
39
|
+
* an implementation resolves that guild's destination preference from, so the
|
|
40
|
+
* caller does not have to know one exists.
|
|
41
|
+
*/
|
|
42
|
+
exportChannel(
|
|
43
|
+
channelId: string,
|
|
44
|
+
deliverToChannelId: string,
|
|
45
|
+
guildId: string,
|
|
46
|
+
options?: ChannelExportOptions,
|
|
47
|
+
): Promise<ChannelExportOutcome>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Interaction } from "discord.js";
|
|
2
|
+
import type { SlashCommand } from "@/discord/command/types";
|
|
3
|
+
import type { InteractionDispatcher } from "@/discord/interaction/interaction-router";
|
|
4
|
+
import type { Logger } from "@/logger";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The slice of the command registry this dispatcher needs. Depending on a
|
|
8
|
+
* one-method port rather than on `CommandRouter` keeps the two dispatchers
|
|
9
|
+
* decoupled while sharing a single registration — the alternative, a second map
|
|
10
|
+
* of commands, would drift the moment a command is registered in only one.
|
|
11
|
+
*/
|
|
12
|
+
export interface CommandLookup {
|
|
13
|
+
get(name: string): SlashCommand | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Answers option autocomplete requests. It is an {@link InteractionDispatcher}
|
|
18
|
+
* aggregated by the shared {@link InteractionRouter}, never a second
|
|
19
|
+
* `client.on("interactionCreate")` — the whole app keeps exactly one listener.
|
|
20
|
+
*/
|
|
21
|
+
export class AutocompleteDispatcher implements InteractionDispatcher {
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly commands: CommandLookup,
|
|
24
|
+
private readonly logger: Logger,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
/** Claim autocomplete interactions; ignore everything else. */
|
|
28
|
+
async handle(interaction: Interaction): Promise<boolean> {
|
|
29
|
+
if (!interaction.isAutocomplete()) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const command = this.commands.get(interaction.commandName);
|
|
34
|
+
if (!command?.autocomplete) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A failing suggestion must never surface to the user: Discord shows no
|
|
39
|
+
// error for autocomplete, it just leaves the box spinning until it times
|
|
40
|
+
// out. Swallow, log, and let the command reply with an empty list.
|
|
41
|
+
try {
|
|
42
|
+
await command.autocomplete(interaction);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.logger.error(
|
|
45
|
+
{
|
|
46
|
+
command: interaction.commandName,
|
|
47
|
+
err: error instanceof Error ? error.message : String(error),
|
|
48
|
+
},
|
|
49
|
+
"Autocomplete handler failed",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AutocompleteInteraction } from "discord.js";
|
|
2
|
+
import { type CompiledSubcommand, routeKey } from "@/discord/command/create-command";
|
|
3
|
+
import type { Choice, Options } from "@/discord/command/options";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Serving option autocomplete. Kept beside the DSL because resolving *which*
|
|
7
|
+
* option is focused needs the same route table `createCommand` builds — the
|
|
8
|
+
* dispatcher only decides that an interaction is an autocomplete request.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Discord refuses more than this many suggestions in one response. */
|
|
12
|
+
const MAX_CHOICES = 25;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Answer a focused option's autocomplete request.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here is allowed to throw: Discord surfaces no error for a failed
|
|
18
|
+
* autocomplete, it just leaves the input spinning until it times out, so an
|
|
19
|
+
* empty list is the only sane failure mode. The caller logs; the user keeps
|
|
20
|
+
* typing.
|
|
21
|
+
*/
|
|
22
|
+
export async function respondWithSuggestions(
|
|
23
|
+
interaction: AutocompleteInteraction,
|
|
24
|
+
options: Options,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const focused = interaction.options.getFocused(true);
|
|
27
|
+
const option = options[focused.name];
|
|
28
|
+
const resolver = option?.autocomplete;
|
|
29
|
+
|
|
30
|
+
if (!resolver) {
|
|
31
|
+
await interaction.respond([]);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The resolver is the only part that can fail (it usually hits a database).
|
|
36
|
+
// Answering an empty list keeps the picker responsive; rethrowing after the
|
|
37
|
+
// response lets the dispatcher log the cause without leaving the user hanging.
|
|
38
|
+
let choices: readonly Choice[];
|
|
39
|
+
try {
|
|
40
|
+
choices = await resolver(interaction);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
await interaction.respond([]);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await interaction.respond(truncate(choices));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Discord rejects the whole response when it carries more than 25 choices, so a
|
|
51
|
+
* resolver returning more is trimmed rather than failing outright — a partial
|
|
52
|
+
* list is still useful to the user typing.
|
|
53
|
+
*/
|
|
54
|
+
function truncate(choices: readonly Choice[]): Choice[] {
|
|
55
|
+
return choices.slice(0, MAX_CHOICES);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The options a given autocomplete interaction applies to: the focused option
|
|
60
|
+
* belongs to the invoked subcommand, not to the command as a whole.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveRouteOptions(
|
|
63
|
+
interaction: AutocompleteInteraction,
|
|
64
|
+
routes: Map<string, CompiledSubcommand>,
|
|
65
|
+
flatOptions: Options | null,
|
|
66
|
+
): Options {
|
|
67
|
+
if (flatOptions) {
|
|
68
|
+
return flatOptions;
|
|
69
|
+
}
|
|
70
|
+
const group = interaction.options.getSubcommandGroup(false);
|
|
71
|
+
const name = interaction.options.getSubcommand(false);
|
|
72
|
+
if (name === null) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
return routes.get(routeKey(group, name))?.options ?? {};
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type ChatInputCommandInteraction, type EmbedBuilder, MessageFlags } from "discord.js";
|
|
2
|
+
import type { Options, Values } from "@/discord/command/options";
|
|
3
|
+
import type { Presenter } from "@/discord/presenter";
|
|
4
|
+
import { type Locale, resolveLocale, type TranslationParams, type Translator } from "@/i18n";
|
|
5
|
+
import type { Logger } from "@/logger";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The handler-facing surface of a dispatched subcommand: the raw interaction,
|
|
9
|
+
* the parsed & typed options, the locale resolved for this interaction, a
|
|
10
|
+
* translate helper bound to it, and convenience responders that route through
|
|
11
|
+
* the injected {@link Presenter}.
|
|
12
|
+
*/
|
|
13
|
+
export interface Context<O extends Options> {
|
|
14
|
+
readonly interaction: ChatInputCommandInteraction;
|
|
15
|
+
readonly options: Values<O>;
|
|
16
|
+
/** Reply language: user's client locale → guild locale → configured default. */
|
|
17
|
+
readonly locale: Locale;
|
|
18
|
+
/** Translate a catalog key for this interaction's locale. */
|
|
19
|
+
t(key: string, params?: TranslationParams): string;
|
|
20
|
+
reply(embed: EmbedBuilder): Promise<void>;
|
|
21
|
+
confirm(message: string): Promise<void>;
|
|
22
|
+
error(message: string): Promise<void>;
|
|
23
|
+
deny(message: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Deliver an embed regardless of the interaction's lifecycle state, and never
|
|
28
|
+
* throw: a failed delivery (e.g. an expired/unknown interaction, code 10062)
|
|
29
|
+
* must not escape a handler and reach the client's unhandled `error` event,
|
|
30
|
+
* which would crash the process. Visibility on a deferred interaction is fixed
|
|
31
|
+
* at deferReply time, so editReply carries no flags.
|
|
32
|
+
*/
|
|
33
|
+
export async function sendEmbed(
|
|
34
|
+
interaction: ChatInputCommandInteraction,
|
|
35
|
+
embed: EmbedBuilder,
|
|
36
|
+
ephemeral: boolean,
|
|
37
|
+
logger: Logger,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const flags = ephemeral ? MessageFlags.Ephemeral : undefined;
|
|
40
|
+
try {
|
|
41
|
+
if (interaction.deferred) {
|
|
42
|
+
await interaction.editReply({ embeds: [embed] });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (interaction.replied) {
|
|
46
|
+
await interaction.followUp({ embeds: [embed], flags });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await interaction.reply({ embeds: [embed], flags });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error(
|
|
52
|
+
{
|
|
53
|
+
commandName: interaction.commandName,
|
|
54
|
+
interactionId: interaction.id,
|
|
55
|
+
err: error instanceof Error ? error.message : String(error),
|
|
56
|
+
},
|
|
57
|
+
"Failed to deliver interaction response",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createContext<O extends Options>(
|
|
63
|
+
interaction: ChatInputCommandInteraction,
|
|
64
|
+
options: Values<O>,
|
|
65
|
+
ephemeral: boolean,
|
|
66
|
+
presenter: Presenter,
|
|
67
|
+
logger: Logger,
|
|
68
|
+
translator: Translator,
|
|
69
|
+
): Context<O> {
|
|
70
|
+
const locale = resolveLocale(
|
|
71
|
+
[interaction.locale, interaction.guildLocale],
|
|
72
|
+
translator.defaultLocale,
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
interaction,
|
|
76
|
+
options,
|
|
77
|
+
locale,
|
|
78
|
+
t: (key, params) => translator.translate(locale, key, params),
|
|
79
|
+
reply: (embed) => sendEmbed(interaction, embed, ephemeral, logger),
|
|
80
|
+
confirm: (message) =>
|
|
81
|
+
sendEmbed(interaction, presenter.confirmation(message, locale), ephemeral, logger),
|
|
82
|
+
error: (message) => sendEmbed(interaction, presenter.error(message, locale), ephemeral, logger),
|
|
83
|
+
deny: (message) => sendEmbed(interaction, presenter.denial(message, locale), ephemeral, logger),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CommandInteraction } from "discord.js";
|
|
2
|
+
import { type Clock, systemClock } from "@/clock";
|
|
3
|
+
import type { Guard, GuardResult } from "@/discord/command/guard";
|
|
4
|
+
import { CORE_MESSAGES } from "@/discord/i18n";
|
|
5
|
+
import type { LocalizedText } from "@/i18n";
|
|
6
|
+
|
|
7
|
+
const ALLOW: GuardResult = { ok: true };
|
|
8
|
+
const MILLISECONDS_PER_SECOND = 1000;
|
|
9
|
+
|
|
10
|
+
/** How a cooldown window is shared between callers. */
|
|
11
|
+
export type CooldownScope = "user" | "guild" | "global";
|
|
12
|
+
|
|
13
|
+
export interface CooldownOptions {
|
|
14
|
+
/** Minimum delay between two accepted invocations, in milliseconds. */
|
|
15
|
+
windowMs: number;
|
|
16
|
+
/** Who shares the window. Defaults to `user`. */
|
|
17
|
+
scope?: CooldownScope;
|
|
18
|
+
/** Denial message; receives the whole seconds left before the next attempt. */
|
|
19
|
+
message?: (secondsLeft: number) => LocalizedText;
|
|
20
|
+
/** Injected for tests; defaults to the system clock. */
|
|
21
|
+
clock?: Clock;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function defaultMessage(secondsLeft: number): LocalizedText {
|
|
25
|
+
return { key: CORE_MESSAGES.cooldownWait, params: { seconds: secondsLeft } };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The key sharing one cooldown window. `global` is a constant so every caller
|
|
30
|
+
* collides on purpose; `guild` falls back to the user outside a guild, so a DM
|
|
31
|
+
* can never bypass a guild-scoped cooldown by having no guild id.
|
|
32
|
+
*/
|
|
33
|
+
function cooldownKey(interaction: CommandInteraction, scope: CooldownScope): string {
|
|
34
|
+
switch (scope) {
|
|
35
|
+
case "global":
|
|
36
|
+
return `${interaction.commandName}:global`;
|
|
37
|
+
case "guild":
|
|
38
|
+
return `${interaction.commandName}:guild:${interaction.guildId ?? interaction.user.id}`;
|
|
39
|
+
case "user":
|
|
40
|
+
return `${interaction.commandName}:user:${interaction.user.id}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Rate-limit a command per user, per guild or globally.
|
|
46
|
+
*
|
|
47
|
+
* Expired entries are pruned on access rather than by a timer: the map is only
|
|
48
|
+
* read when the command runs, so a background interval would keep the process
|
|
49
|
+
* awake for no benefit. Each guard instance owns its map, so two commands never
|
|
50
|
+
* share a window unless they share the guard.
|
|
51
|
+
*/
|
|
52
|
+
export function createCooldownGuard(options: CooldownOptions): Guard {
|
|
53
|
+
const { windowMs, scope = "user", message = defaultMessage, clock = systemClock } = options;
|
|
54
|
+
const lastUsed = new Map<string, number>();
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
check(interaction: CommandInteraction): GuardResult {
|
|
58
|
+
const now = clock.now().getTime();
|
|
59
|
+
const key = cooldownKey(interaction, scope);
|
|
60
|
+
const previous = lastUsed.get(key);
|
|
61
|
+
|
|
62
|
+
if (previous !== undefined && now - previous < windowMs) {
|
|
63
|
+
const secondsLeft = Math.ceil((windowMs - (now - previous)) / MILLISECONDS_PER_SECOND);
|
|
64
|
+
return { ok: false, message: message(secondsLeft) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lastUsed.set(key, now);
|
|
68
|
+
pruneExpired(lastUsed, now, windowMs);
|
|
69
|
+
return ALLOW;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Drop entries whose window has elapsed, so the map cannot grow without bound. */
|
|
75
|
+
function pruneExpired(lastUsed: Map<string, number>, now: number, windowMs: number): void {
|
|
76
|
+
for (const [key, timestamp] of lastUsed) {
|
|
77
|
+
if (now - timestamp >= windowMs) {
|
|
78
|
+
lastUsed.delete(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|