@nubisjs/core 0.0.0-stage → 0.1.0-next.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 +6 -2
- package/dist/api.d.ts +14 -0
- package/dist/autocomplete.d.ts +37 -0
- package/dist/command.d.ts +67 -0
- package/dist/component-dispatch.d.ts +45 -0
- package/dist/component.d.ts +214 -0
- package/dist/custom-id.d.ts +26 -0
- package/dist/dispatch.d.ts +59 -0
- package/dist/errors.d.ts +19 -0
- package/dist/events.d.ts +57 -0
- package/dist/guard.d.ts +62 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +1719 -0
- package/dist/lifecycle.d.ts +55 -0
- package/dist/menu.d.ts +79 -0
- package/dist/middleware.d.ts +15 -0
- package/dist/modal.d.ts +75 -0
- package/dist/names.d.ts +2 -0
- package/dist/option.d.ts +93 -0
- package/dist/reply.d.ts +57 -0
- package/dist/schema.d.ts +32 -0
- package/dist/serialize.d.ts +8 -0
- package/dist/store.d.ts +35 -0
- package/dist/sync.d.ts +23 -0
- package/dist/tree.d.ts +50 -0
- package/package.json +39 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EYTAK LIMITED
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nubisjs/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The engine behind [Nubis](https://eytak.com/nubis): command, component and modal definitions with inferred types, guards and middleware, Discord's rules checked at definition time, and dispatch with the reply lifecycle.
|
|
4
|
+
|
|
5
|
+
Most bots should install [`nubis`](https://www.npmjs.com/package/nubis), which re-exports everything here.
|
|
6
|
+
|
|
7
|
+
MIT. Built by [Eytak](https://eytak.com).
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Discord API client handlers get as `api`. Empty here so core stays
|
|
3
|
+
* free of any HTTP library; the `nubis` package fills it in with
|
|
4
|
+
* @discordjs/core's typed API through module augmentation:
|
|
5
|
+
*
|
|
6
|
+
* declare module "@nubisjs/core" { interface NubisApi extends API {} }
|
|
7
|
+
*/
|
|
8
|
+
export interface NubisApi {
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Stands in when no API client was given: any use explains how to get one,
|
|
12
|
+
* instead of failing with "cannot read properties of undefined".
|
|
13
|
+
*/
|
|
14
|
+
export declare function missingApi(): NubisApi;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type APIApplicationCommandAutocompleteInteraction } from "discord-api-types/v10";
|
|
2
|
+
import { type NubisApi } from "./api";
|
|
3
|
+
import { type ErrorHandler } from "./errors";
|
|
4
|
+
import { type Guard } from "./guard";
|
|
5
|
+
import type { Transport } from "./lifecycle";
|
|
6
|
+
import type { CommandTree } from "./tree";
|
|
7
|
+
export interface AutocompleteOptions {
|
|
8
|
+
/** The bot's guards; see DispatchOptions.guards. */
|
|
9
|
+
readonly guards?: readonly Guard[];
|
|
10
|
+
readonly onError?: ErrorHandler;
|
|
11
|
+
readonly api?: NubisApi;
|
|
12
|
+
}
|
|
13
|
+
export type AutocompleteResult = {
|
|
14
|
+
readonly status: "handled";
|
|
15
|
+
readonly path: readonly string[];
|
|
16
|
+
readonly count: number;
|
|
17
|
+
}
|
|
18
|
+
/** No command, no focused option, or the option has no autocomplete handler. */
|
|
19
|
+
| {
|
|
20
|
+
readonly status: "unknown";
|
|
21
|
+
readonly path: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/** A guard denied: no suggestions, so they can't leak what the command would show. */
|
|
24
|
+
| {
|
|
25
|
+
readonly status: "denied";
|
|
26
|
+
readonly path: readonly string[];
|
|
27
|
+
} | {
|
|
28
|
+
readonly status: "error";
|
|
29
|
+
readonly path: readonly string[];
|
|
30
|
+
readonly error: unknown;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Answers an autocomplete interaction with the focused option's suggestions.
|
|
34
|
+
* Every outcome answers, with an empty list when there's nothing to suggest,
|
|
35
|
+
* because Discord shows an error for autocomplete that doesn't respond.
|
|
36
|
+
*/
|
|
37
|
+
export declare function dispatchAutocomplete(tree: CommandTree, interaction: APIApplicationCommandAutocompleteInteraction, transport: Transport, options?: AutocompleteOptions): Promise<AutocompleteResult>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { APIChatInputApplicationCommandInteraction } from "discord-api-types/v10";
|
|
2
|
+
import type { NubisApi } from "./api";
|
|
3
|
+
import type { ModalData } from "./component";
|
|
4
|
+
import type { Guard } from "./guard";
|
|
5
|
+
import type { Middleware } from "./middleware";
|
|
6
|
+
import type { OptionMap, ResolvedOptions } from "./option";
|
|
7
|
+
import type { ReplyInput } from "./reply";
|
|
8
|
+
/** Where a command can be used: servers, the bot's DMs, or group DMs and other users' DMs. */
|
|
9
|
+
export type CommandContextName = "guild" | "bot-dm" | "private-channel";
|
|
10
|
+
/** How the app is installed: to a server, or to a user's account. */
|
|
11
|
+
export type IntegrationTypeName = "guild" | "user";
|
|
12
|
+
/** Settings Discord only reads on top-level commands; folders can set them too. */
|
|
13
|
+
export interface TopLevelSettings {
|
|
14
|
+
readonly defaultMemberPermissions?: readonly bigint[];
|
|
15
|
+
readonly contexts?: readonly CommandContextName[];
|
|
16
|
+
readonly integrationTypes?: readonly IntegrationTypeName[];
|
|
17
|
+
readonly nsfw?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface CommandContext<T extends OptionMap> {
|
|
20
|
+
readonly options: ResolvedOptions<T>;
|
|
21
|
+
/** The raw interaction from Discord, for anything Nubis doesn't cover yet. */
|
|
22
|
+
readonly interaction: APIChatInputApplicationCommandInteraction;
|
|
23
|
+
/** Discord's API: ban members, send messages, look things up. */
|
|
24
|
+
readonly api: NubisApi;
|
|
25
|
+
/** Sends a message. The first one answers the command, later ones are follow-ups. */
|
|
26
|
+
reply(message: ReplyInput): Promise<void>;
|
|
27
|
+
/** Edits the first reply, e.g. to show progress. */
|
|
28
|
+
editReply(message: ReplyInput): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Shows "thinking…" for work over three seconds. Nubis does this on its
|
|
31
|
+
* own after two seconds; call it earlier to choose `ephemeral` yourself.
|
|
32
|
+
*/
|
|
33
|
+
defer(options?: {
|
|
34
|
+
readonly ephemeral?: boolean;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
/** Opens a modal. Must be the first response, so call it before reply(). */
|
|
37
|
+
showModal(modal: ModalData): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export interface CommandConfig<T extends OptionMap> {
|
|
40
|
+
readonly description: string;
|
|
41
|
+
readonly options?: T;
|
|
42
|
+
/** Makes replies (and the automatic "thinking…") visible only to the user. */
|
|
43
|
+
readonly ephemeral?: boolean;
|
|
44
|
+
/** Checks that run before `run`, after the folder's and the bot's own. */
|
|
45
|
+
readonly guards?: readonly Guard[];
|
|
46
|
+
/** Wraps `run`, inside the folder's and the bot's own middleware. */
|
|
47
|
+
readonly middleware?: readonly Middleware[];
|
|
48
|
+
/**
|
|
49
|
+
* Hides the command from members without these permissions
|
|
50
|
+
* (`PermissionFlagsBits`). Server admins can change this in Discord's
|
|
51
|
+
* settings, so pair it with a `hasPermission` guard when it matters.
|
|
52
|
+
* Top-level commands only; for subcommands, set it on the folder.
|
|
53
|
+
*/
|
|
54
|
+
readonly defaultMemberPermissions?: readonly bigint[];
|
|
55
|
+
/** Top-level only. Where the command can be used. Defaults to everywhere it's installed. */
|
|
56
|
+
readonly contexts?: readonly CommandContextName[];
|
|
57
|
+
/** Top-level only. "user" makes it usable when installed to a user's account. */
|
|
58
|
+
readonly integrationTypes?: readonly IntegrationTypeName[];
|
|
59
|
+
/** Top-level only. Age-restricted: only shown in age-restricted channels. */
|
|
60
|
+
readonly nsfw?: boolean;
|
|
61
|
+
run(context: CommandContext<T>): unknown;
|
|
62
|
+
}
|
|
63
|
+
export interface CommandDefinition<T extends OptionMap> extends CommandConfig<T> {
|
|
64
|
+
readonly kind: "command";
|
|
65
|
+
readonly options: T;
|
|
66
|
+
}
|
|
67
|
+
export declare function command<T extends OptionMap = Record<never, never>>(config: CommandConfig<T>): CommandDefinition<T>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { APIMessageComponentInteraction, APIModalSubmitInteraction } from "discord-api-types/v10";
|
|
2
|
+
import { type ComponentDefinition, type ComponentRuntime } from "./component";
|
|
3
|
+
import type { NubisApi } from "./api";
|
|
4
|
+
import { type ErrorHandler } from "./errors";
|
|
5
|
+
import { type Guard } from "./guard";
|
|
6
|
+
import { type Middleware } from "./middleware";
|
|
7
|
+
import { type Scheduler, type Transport } from "./lifecycle";
|
|
8
|
+
export type ComponentRegistry = ReadonlyMap<string, ComponentRuntime>;
|
|
9
|
+
export declare function createComponentRegistry(components: readonly ComponentDefinition[]): ComponentRegistry;
|
|
10
|
+
export type ComponentDispatchResult = {
|
|
11
|
+
readonly status: "handled";
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly responded: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
readonly status: "error";
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly error: unknown;
|
|
18
|
+
} | {
|
|
19
|
+
readonly status: "denied";
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly status: "unknown";
|
|
24
|
+
readonly customId: string;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: "stale";
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly reason: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const STALE_MESSAGE = "This no longer works. It may be from an older message; try the command again.";
|
|
31
|
+
export interface ComponentDispatchOptions {
|
|
32
|
+
readonly autoDeferMs?: number | false;
|
|
33
|
+
readonly schedule?: Scheduler;
|
|
34
|
+
/** Run before every component and modal handler, before their own guards. */
|
|
35
|
+
readonly guards?: readonly Guard[];
|
|
36
|
+
readonly middleware?: readonly Middleware[];
|
|
37
|
+
/** Receives every error a handler throws, after the user is told. */
|
|
38
|
+
readonly onError?: ErrorHandler;
|
|
39
|
+
/** Discord's API client, handed to handlers as `api`. */
|
|
40
|
+
readonly api?: NubisApi;
|
|
41
|
+
}
|
|
42
|
+
/** Routes a button click or select to its component, with its state validated. */
|
|
43
|
+
export declare function dispatchComponent(registry: ComponentRegistry, interaction: APIMessageComponentInteraction, transport: Transport, options?: ComponentDispatchOptions): Promise<ComponentDispatchResult>;
|
|
44
|
+
/** Routes a modal submission to its modal, with state and values validated. */
|
|
45
|
+
export declare function dispatchModal(registry: ComponentRegistry, interaction: APIModalSubmitInteraction, transport: Transport, options?: ComponentDispatchOptions): Promise<ComponentDispatchResult>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { type APIButtonComponentWithCustomId, type APIButtonComponentWithURL, type APIChannelSelectComponent, type APIInteractionDataResolvedChannel, type APIInteractionDataResolvedGuildMember, type APIMentionableSelectComponent, type APIMessageComponentEmoji, type APIMessageComponentInteraction, type APIModalInteractionResponseCallbackData, type APIModalSubmitInteraction, type APIRole, type APIRoleSelectComponent, type APIStringSelectComponent, type APIUser, type APIUserSelectComponent, ButtonStyle, type ChannelType } from "discord-api-types/v10";
|
|
2
|
+
import type { NubisApi } from "./api";
|
|
3
|
+
import type { Guard } from "./guard";
|
|
4
|
+
import type { Middleware } from "./middleware";
|
|
5
|
+
import type { ReplyInput } from "./reply";
|
|
6
|
+
import type { SchemaInput, SchemaOutput, StandardSchemaV1 } from "./schema";
|
|
7
|
+
import type { StateStore } from "./store";
|
|
8
|
+
export type AnySchema = StandardSchemaV1<unknown, unknown>;
|
|
9
|
+
/** A rendered modal, ready for `showModal()`. */
|
|
10
|
+
export type ModalData = APIModalInteractionResponseCallbackData;
|
|
11
|
+
/** State inside handlers: the schema's output, or `undefined` without one. */
|
|
12
|
+
export type ComponentState<S> = [S] extends [undefined] ? undefined : SchemaOutput<S>;
|
|
13
|
+
/** Arguments for rendering: state first when the component has a schema. */
|
|
14
|
+
export type RenderArgs<S, O> = [S] extends [undefined] ? [overrides?: O] : [state: SchemaInput<S>, overrides?: O];
|
|
15
|
+
export interface ComponentContext<State> {
|
|
16
|
+
readonly state: State;
|
|
17
|
+
/** The raw interaction from Discord, for anything Nubis doesn't cover yet. */
|
|
18
|
+
readonly interaction: APIMessageComponentInteraction;
|
|
19
|
+
/** Discord's API: act on what was clicked. */
|
|
20
|
+
readonly api: NubisApi;
|
|
21
|
+
/** Sends a new message. */
|
|
22
|
+
reply(message: ReplyInput): Promise<void>;
|
|
23
|
+
/** Edits the message the component is on. Call it before reply(). */
|
|
24
|
+
update(message: ReplyInput): Promise<void>;
|
|
25
|
+
/** Edits the first reply. */
|
|
26
|
+
editReply(message: ReplyInput): Promise<void>;
|
|
27
|
+
/** Shows "thinking…" and a reply to come. */
|
|
28
|
+
defer(options?: {
|
|
29
|
+
readonly ephemeral?: boolean;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Acknowledges the click without a message; update() can still edit later.
|
|
33
|
+
* Nubis does this on its own after two seconds.
|
|
34
|
+
*/
|
|
35
|
+
deferUpdate(): Promise<void>;
|
|
36
|
+
/** Opens a modal. Must be the first response, so call it before reply(). */
|
|
37
|
+
showModal(modal: ModalData): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/** The parts of a handler context that don't depend on the interaction type. */
|
|
40
|
+
export interface HandlerBase {
|
|
41
|
+
readonly state: unknown;
|
|
42
|
+
readonly api: NubisApi;
|
|
43
|
+
reply(message: ReplyInput): Promise<void>;
|
|
44
|
+
update(message: ReplyInput): Promise<void>;
|
|
45
|
+
editReply(message: ReplyInput): Promise<void>;
|
|
46
|
+
defer(options?: {
|
|
47
|
+
readonly ephemeral?: boolean;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
deferUpdate(): Promise<void>;
|
|
50
|
+
showModal(modal: ModalData): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
export type ComponentKind = "button" | "string-select" | "user-select" | "role-select" | "channel-select" | "mentionable-select" | "modal";
|
|
53
|
+
/** What dispatch needs from any component or modal, whatever its state type. */
|
|
54
|
+
export interface ComponentRuntime {
|
|
55
|
+
readonly kind: ComponentKind;
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly schema: AnySchema | undefined;
|
|
58
|
+
/** Where state lives when it's kept outside the custom_id. */
|
|
59
|
+
readonly store: StateStore | undefined;
|
|
60
|
+
readonly guards: readonly Guard[];
|
|
61
|
+
readonly middleware: readonly Middleware[];
|
|
62
|
+
/** Returns false when the interaction doesn't fit (a stale message or form). */
|
|
63
|
+
handle(base: HandlerBase, interaction: APIMessageComponentInteraction | APIModalSubmitInteraction): Promise<boolean>;
|
|
64
|
+
}
|
|
65
|
+
export declare const RUNTIME = "~nubis";
|
|
66
|
+
export interface WithRuntime {
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly [RUNTIME]: ComponentRuntime;
|
|
69
|
+
}
|
|
70
|
+
/** Settings every interactive component shares. */
|
|
71
|
+
interface InteractiveConfig<S extends AnySchema | undefined> {
|
|
72
|
+
/** Routes clicks to this component. Changing it makes ones already sent stop working. */
|
|
73
|
+
readonly id: string;
|
|
74
|
+
/** Validates the state that comes back with each interaction. */
|
|
75
|
+
readonly state?: S;
|
|
76
|
+
/**
|
|
77
|
+
* Keeps state here instead of in the custom_id, for state over Discord's
|
|
78
|
+
* 100 characters. The custom_id then only carries a key.
|
|
79
|
+
*/
|
|
80
|
+
readonly store?: StateStore;
|
|
81
|
+
/**
|
|
82
|
+
* Checks before the handler. Anyone who can see a message can click its
|
|
83
|
+
* components, so use `invokerOnly` or a permission guard for anything that
|
|
84
|
+
* acts on someone's behalf.
|
|
85
|
+
*/
|
|
86
|
+
readonly guards?: readonly Guard[];
|
|
87
|
+
readonly middleware?: readonly Middleware[];
|
|
88
|
+
}
|
|
89
|
+
/** "🔥", "<:party:123>", "<a:dance:456>", or Discord's emoji object. */
|
|
90
|
+
export type EmojiInput = string | APIMessageComponentEmoji;
|
|
91
|
+
export declare function toEmoji(input: EmojiInput): APIMessageComponentEmoji;
|
|
92
|
+
declare const BUTTON_STYLES: {
|
|
93
|
+
readonly primary: ButtonStyle.Primary;
|
|
94
|
+
readonly secondary: ButtonStyle.Secondary;
|
|
95
|
+
readonly success: ButtonStyle.Success;
|
|
96
|
+
readonly danger: ButtonStyle.Danger;
|
|
97
|
+
};
|
|
98
|
+
export type ButtonStyleName = keyof typeof BUTTON_STYLES;
|
|
99
|
+
export interface ButtonOverrides {
|
|
100
|
+
readonly label?: string;
|
|
101
|
+
readonly style?: ButtonStyleName;
|
|
102
|
+
readonly emoji?: EmojiInput;
|
|
103
|
+
readonly disabled?: boolean;
|
|
104
|
+
}
|
|
105
|
+
export interface ButtonConfig<S extends AnySchema | undefined> extends InteractiveConfig<S> {
|
|
106
|
+
readonly label: string;
|
|
107
|
+
readonly style?: ButtonStyleName;
|
|
108
|
+
readonly emoji?: EmojiInput;
|
|
109
|
+
onClick(context: ComponentContext<ComponentState<S>>): unknown;
|
|
110
|
+
}
|
|
111
|
+
export interface ButtonDefinition<S extends AnySchema | undefined> extends WithRuntime {
|
|
112
|
+
(...args: RenderArgs<S, ButtonOverrides>): APIButtonComponentWithCustomId;
|
|
113
|
+
readonly kind: "button";
|
|
114
|
+
}
|
|
115
|
+
export declare function button<S extends AnySchema | undefined = undefined>(config: ButtonConfig<S>): ButtonDefinition<S>;
|
|
116
|
+
export interface LinkButtonConfig {
|
|
117
|
+
readonly url: string;
|
|
118
|
+
readonly label?: string;
|
|
119
|
+
readonly emoji?: EmojiInput;
|
|
120
|
+
readonly disabled?: boolean;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A button that opens a URL. It has no handler: Discord opens the link
|
|
124
|
+
* without telling the bot, so it needs no id and no registration.
|
|
125
|
+
*/
|
|
126
|
+
export declare function linkButton(config: LinkButtonConfig): APIButtonComponentWithURL;
|
|
127
|
+
export interface SelectOption<V extends string = string> {
|
|
128
|
+
readonly label: string;
|
|
129
|
+
readonly value: V;
|
|
130
|
+
readonly description?: string;
|
|
131
|
+
readonly emoji?: EmojiInput;
|
|
132
|
+
readonly default?: boolean;
|
|
133
|
+
}
|
|
134
|
+
export interface StringSelectOverrides {
|
|
135
|
+
readonly placeholder?: string;
|
|
136
|
+
readonly disabled?: boolean;
|
|
137
|
+
}
|
|
138
|
+
export interface StringSelectConfig<O extends readonly SelectOption[], S extends AnySchema | undefined> extends InteractiveConfig<S> {
|
|
139
|
+
readonly options: O;
|
|
140
|
+
readonly placeholder?: string;
|
|
141
|
+
readonly minValues?: number;
|
|
142
|
+
readonly maxValues?: number;
|
|
143
|
+
onSelect(context: ComponentContext<ComponentState<S>> & {
|
|
144
|
+
/** The chosen values, typed from `options`. */
|
|
145
|
+
readonly values: O[number]["value"][];
|
|
146
|
+
}): unknown;
|
|
147
|
+
}
|
|
148
|
+
export interface StringSelectDefinition<O extends readonly SelectOption[], S extends AnySchema | undefined> extends WithRuntime {
|
|
149
|
+
(...args: RenderArgs<S, StringSelectOverrides>): APIStringSelectComponent;
|
|
150
|
+
readonly kind: "string-select";
|
|
151
|
+
readonly options: O;
|
|
152
|
+
}
|
|
153
|
+
export declare function stringSelect<const O extends readonly SelectOption[], S extends AnySchema | undefined = undefined>(config: StringSelectConfig<O, S>): StringSelectDefinition<O, S>;
|
|
154
|
+
/** What each select hands its handler: Discord's resolved objects, not bare ids. */
|
|
155
|
+
export interface UserSelection {
|
|
156
|
+
readonly users: APIUser[];
|
|
157
|
+
/** Server memberships by user id, when used in a server. */
|
|
158
|
+
readonly members: Readonly<Record<string, APIInteractionDataResolvedGuildMember | undefined>>;
|
|
159
|
+
}
|
|
160
|
+
export interface RoleSelection {
|
|
161
|
+
readonly roles: APIRole[];
|
|
162
|
+
}
|
|
163
|
+
export interface ChannelSelection {
|
|
164
|
+
readonly channels: APIInteractionDataResolvedChannel[];
|
|
165
|
+
}
|
|
166
|
+
export interface MentionableSelection {
|
|
167
|
+
readonly users: APIUser[];
|
|
168
|
+
readonly roles: APIRole[];
|
|
169
|
+
}
|
|
170
|
+
interface EntitySelectOverrides {
|
|
171
|
+
readonly placeholder?: string;
|
|
172
|
+
readonly disabled?: boolean;
|
|
173
|
+
}
|
|
174
|
+
export interface UserSelectOverrides extends EntitySelectOverrides {
|
|
175
|
+
/** Users selected when the menu appears. */
|
|
176
|
+
readonly defaultUsers?: readonly string[];
|
|
177
|
+
}
|
|
178
|
+
export interface RoleSelectOverrides extends EntitySelectOverrides {
|
|
179
|
+
readonly defaultRoles?: readonly string[];
|
|
180
|
+
}
|
|
181
|
+
export interface ChannelSelectOverrides extends EntitySelectOverrides {
|
|
182
|
+
readonly defaultChannels?: readonly string[];
|
|
183
|
+
}
|
|
184
|
+
export interface MentionableSelectOverrides extends EntitySelectOverrides {
|
|
185
|
+
readonly defaultUsers?: readonly string[];
|
|
186
|
+
readonly defaultRoles?: readonly string[];
|
|
187
|
+
}
|
|
188
|
+
interface EntitySelectConfig<S extends AnySchema | undefined, Selection> extends InteractiveConfig<S> {
|
|
189
|
+
readonly placeholder?: string;
|
|
190
|
+
readonly minValues?: number;
|
|
191
|
+
readonly maxValues?: number;
|
|
192
|
+
onSelect(context: ComponentContext<ComponentState<S>> & Selection): unknown;
|
|
193
|
+
}
|
|
194
|
+
export type UserSelectConfig<S extends AnySchema | undefined> = EntitySelectConfig<S, UserSelection>;
|
|
195
|
+
export type RoleSelectConfig<S extends AnySchema | undefined> = EntitySelectConfig<S, RoleSelection>;
|
|
196
|
+
export type MentionableSelectConfig<S extends AnySchema | undefined> = EntitySelectConfig<S, MentionableSelection>;
|
|
197
|
+
export interface ChannelSelectConfig<S extends AnySchema | undefined> extends EntitySelectConfig<S, ChannelSelection> {
|
|
198
|
+
/** Only these channel types can be picked. */
|
|
199
|
+
readonly channelTypes?: readonly ChannelType[];
|
|
200
|
+
}
|
|
201
|
+
export interface EntitySelectDefinition<S extends AnySchema | undefined, Overrides, Rendered, Kind extends ComponentKind> extends WithRuntime {
|
|
202
|
+
(...args: RenderArgs<S, Overrides>): Rendered;
|
|
203
|
+
readonly kind: Kind;
|
|
204
|
+
}
|
|
205
|
+
/** Pick server members or users. The handler gets their user objects and memberships. */
|
|
206
|
+
export declare function userSelect<S extends AnySchema | undefined = undefined>(config: UserSelectConfig<S>): EntitySelectDefinition<S, UserSelectOverrides, APIUserSelectComponent, "user-select">;
|
|
207
|
+
/** Pick roles. */
|
|
208
|
+
export declare function roleSelect<S extends AnySchema | undefined = undefined>(config: RoleSelectConfig<S>): EntitySelectDefinition<S, RoleSelectOverrides, APIRoleSelectComponent, "role-select">;
|
|
209
|
+
/** Pick channels, optionally of certain types. */
|
|
210
|
+
export declare function channelSelect<S extends AnySchema | undefined = undefined>(config: ChannelSelectConfig<S>): EntitySelectDefinition<S, ChannelSelectOverrides, APIChannelSelectComponent, "channel-select">;
|
|
211
|
+
/** Pick users and roles in one menu. */
|
|
212
|
+
export declare function mentionableSelect<S extends AnySchema | undefined = undefined>(config: MentionableSelectConfig<S>): EntitySelectDefinition<S, MentionableSelectOverrides, APIMentionableSelectComponent, "mentionable-select">;
|
|
213
|
+
export type ComponentDefinition = WithRuntime;
|
|
214
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type StateStore } from "./store";
|
|
2
|
+
export declare function assertValidComponentId(id: string): void;
|
|
3
|
+
/** `confirm-ban~{"userId":"42"}`: the id routes the interaction, the JSON is the state. */
|
|
4
|
+
export declare function encodeCustomId(id: string, state: unknown): string;
|
|
5
|
+
/** `confirm-ban~$Kq3…`: the state lives in a StateStore under that key. */
|
|
6
|
+
export declare function encodeStoredCustomId(id: string, key: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* The custom_id for a render: state inline as JSON, or with a store, saved
|
|
9
|
+
* under a fresh key. The write is tracked so replies wait for it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderCustomId(id: string, state: unknown, store: StateStore | undefined): string;
|
|
12
|
+
export type DecodedCustomId = {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly state: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** State is in a store under `key`. JSON never starts with "$", so this can't be mistaken for inline state. */
|
|
18
|
+
| {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly ok: true;
|
|
21
|
+
readonly key: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly ok: false;
|
|
25
|
+
};
|
|
26
|
+
export declare function decodeCustomId(customId: string): DecodedCustomId;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type APIApplicationCommandInteractionDataBasicOption, type APIApplicationCommandInteractionDataOption, type APIChatInputApplicationCommandInteraction } from "discord-api-types/v10";
|
|
2
|
+
import type { NubisApi } from "./api";
|
|
3
|
+
import { type ErrorHandler } from "./errors";
|
|
4
|
+
import { type Guard } from "./guard";
|
|
5
|
+
import { type Middleware } from "./middleware";
|
|
6
|
+
import { type Lifecycle, type Scheduler, type Transport } from "./lifecycle";
|
|
7
|
+
import type { CommandLeaf, CommandParent, CommandTree } from "./tree";
|
|
8
|
+
export interface DispatchOptions {
|
|
9
|
+
/** See LifecycleOptions.autoDeferMs. */
|
|
10
|
+
readonly autoDeferMs?: number | false;
|
|
11
|
+
readonly schedule?: Scheduler;
|
|
12
|
+
/** Run before every handler, before folder and command guards. */
|
|
13
|
+
readonly guards?: readonly Guard[];
|
|
14
|
+
/** Wraps every handler, outermost. */
|
|
15
|
+
readonly middleware?: readonly Middleware[];
|
|
16
|
+
/** Receives every error a handler throws, after the user is told. */
|
|
17
|
+
readonly onError?: ErrorHandler;
|
|
18
|
+
/** Discord's API client, handed to handlers as `api`. */
|
|
19
|
+
readonly api?: NubisApi;
|
|
20
|
+
}
|
|
21
|
+
export type DispatchResult = {
|
|
22
|
+
readonly status: "handled";
|
|
23
|
+
readonly path: readonly string[];
|
|
24
|
+
/** False when the handler finished without answering; Discord shows "did not respond". */
|
|
25
|
+
readonly responded: boolean;
|
|
26
|
+
} | {
|
|
27
|
+
readonly status: "unknown";
|
|
28
|
+
readonly path: readonly string[];
|
|
29
|
+
} | {
|
|
30
|
+
readonly status: "denied";
|
|
31
|
+
readonly path: readonly string[];
|
|
32
|
+
readonly message: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly status: "error";
|
|
35
|
+
readonly path: readonly string[];
|
|
36
|
+
readonly error: unknown;
|
|
37
|
+
};
|
|
38
|
+
export declare const ERROR_MESSAGE = "Something went wrong. Please try again.";
|
|
39
|
+
export declare const UNKNOWN_COMMAND_MESSAGE = "This command isn't available anymore. Discord may still be catching up.";
|
|
40
|
+
/** Tells the user something broke, if the interaction can still be answered. */
|
|
41
|
+
export declare function answerError(lifecycle: Lifecycle): Promise<void>;
|
|
42
|
+
export interface Located {
|
|
43
|
+
readonly leaf: CommandLeaf | undefined;
|
|
44
|
+
/** Folders on the way to the leaf, outermost first. */
|
|
45
|
+
readonly parents: readonly CommandParent[];
|
|
46
|
+
readonly path: readonly string[];
|
|
47
|
+
readonly options: readonly APIApplicationCommandInteractionDataBasicOption[];
|
|
48
|
+
}
|
|
49
|
+
/** Finds the command an interaction's data points at, through subcommands and groups. */
|
|
50
|
+
export declare function locate(tree: CommandTree, data: {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly options?: readonly APIApplicationCommandInteractionDataOption[];
|
|
53
|
+
}): Located;
|
|
54
|
+
/**
|
|
55
|
+
* Routes a slash command interaction to the command that handles it, and
|
|
56
|
+
* answers Discord on time: "thinking…" after two seconds of silence, follow-ups
|
|
57
|
+
* for later replies, and an error message if the handler throws.
|
|
58
|
+
*/
|
|
59
|
+
export declare function dispatch(tree: CommandTree, interaction: APIChatInputApplicationCommandInteraction, transport: Transport, options?: DispatchOptions): Promise<DispatchResult>;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Thrown for mistakes in a bot's own definitions, with the file or command it came from. */
|
|
2
|
+
export declare class NubisError extends Error {
|
|
3
|
+
readonly name = "NubisError";
|
|
4
|
+
}
|
|
5
|
+
/** Where a handler error happened, for logging and error trackers. */
|
|
6
|
+
export interface ErrorInfo {
|
|
7
|
+
readonly kind: "command" | "menu" | "component" | "modal" | "autocomplete" | "event";
|
|
8
|
+
/** "/config set", a component id, a menu name, or an event name. */
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/** The interaction, for everything but events. */
|
|
11
|
+
readonly interaction?: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Receives every error a handler throws, after the user has been told
|
|
15
|
+
* something went wrong. A good place for logging or Sentry.
|
|
16
|
+
*/
|
|
17
|
+
export type ErrorHandler = (error: unknown, info: ErrorInfo) => void;
|
|
18
|
+
/** Calls the error handler without letting it break the dispatch. */
|
|
19
|
+
export declare function reportError(onError: ErrorHandler | undefined, error: unknown, info: ErrorInfo): void;
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { GatewayDispatchPayload } from "discord-api-types/v10";
|
|
2
|
+
import { type NubisApi } from "./api";
|
|
3
|
+
import { type ErrorHandler } from "./errors";
|
|
4
|
+
type DispatchData = {
|
|
5
|
+
[P in GatewayDispatchPayload as P["t"]]: P["d"];
|
|
6
|
+
};
|
|
7
|
+
type PascalTail<S extends string> = S extends `${infer Head}_${infer Rest}` ? `${Capitalize<Lowercase<Head>>}${PascalTail<Rest>}` : Capitalize<Lowercase<S>>;
|
|
8
|
+
/** "GUILD_MEMBER_ADD" → "guildMemberAdd". */
|
|
9
|
+
export type CamelCase<S extends string> = S extends `${infer Head}_${infer Rest}` ? `${Lowercase<Head>}${PascalTail<Rest>}` : Lowercase<S>;
|
|
10
|
+
/** Gateway events by their Nubis name, with Discord's payload for each. */
|
|
11
|
+
export type EventPayloads = {
|
|
12
|
+
[K in keyof DispatchData as CamelCase<K & string>]: DispatchData[K];
|
|
13
|
+
};
|
|
14
|
+
export type EventName = keyof EventPayloads;
|
|
15
|
+
export interface EventContext {
|
|
16
|
+
readonly name: EventName;
|
|
17
|
+
/** The shard that received the event, when the transport shards. */
|
|
18
|
+
readonly shardId: number | undefined;
|
|
19
|
+
/** Discord's API, to act on the event. */
|
|
20
|
+
readonly api: NubisApi;
|
|
21
|
+
}
|
|
22
|
+
export interface EventDefinition<N extends EventName = EventName> {
|
|
23
|
+
readonly kind: "event";
|
|
24
|
+
readonly name: N;
|
|
25
|
+
readonly handler: (data: EventPayloads[N], context: EventContext) => unknown;
|
|
26
|
+
}
|
|
27
|
+
/** Discord's event name in Nubis form, the same conversion the types do. */
|
|
28
|
+
export declare function toEventName(type: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Runs `handler` for a gateway event, with Discord's payload typed:
|
|
31
|
+
*
|
|
32
|
+
* event("guildMemberAdd", (member) => member.user.username)
|
|
33
|
+
*/
|
|
34
|
+
export declare function event<N extends EventName>(name: N, handler: (data: EventPayloads[N], context: EventContext) => unknown): EventDefinition<N>;
|
|
35
|
+
/** Any one event's definition. A union, so each keeps its own payload type. */
|
|
36
|
+
export type AnyEventDefinition = {
|
|
37
|
+
[N in EventName]: EventDefinition<N>;
|
|
38
|
+
}[EventName];
|
|
39
|
+
export type EventRegistry = ReadonlyMap<string, readonly AnyEventDefinition[]>;
|
|
40
|
+
/** Groups handlers by event. Several handlers can listen to the same event. */
|
|
41
|
+
export declare function createEventRegistry(events: readonly AnyEventDefinition[]): EventRegistry;
|
|
42
|
+
export interface EventDispatchResult {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
/** How many handlers ran to completion. */
|
|
45
|
+
readonly handled: number;
|
|
46
|
+
readonly errors: readonly unknown[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Runs every handler for a gateway dispatch (`t` and `d` as Discord sends
|
|
50
|
+
* them). Handlers run concurrently; one throwing doesn't stop the others.
|
|
51
|
+
*/
|
|
52
|
+
export declare function dispatchEvent(registry: EventRegistry, type: string, data: unknown, options?: {
|
|
53
|
+
readonly shardId?: number;
|
|
54
|
+
readonly onError?: ErrorHandler;
|
|
55
|
+
readonly api?: NubisApi;
|
|
56
|
+
}): Promise<EventDispatchResult>;
|
|
57
|
+
export {};
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { APIApplicationCommandAutocompleteInteraction, APIChatInputApplicationCommandInteraction, APIContextMenuInteraction, APIInteractionGuildMember, APIMessageComponentInteraction, APIModalSubmitInteraction, APIUser } from "discord-api-types/v10";
|
|
2
|
+
import { type NubisApi } from "./api";
|
|
3
|
+
export type GuardedInteraction = APIApplicationCommandAutocompleteInteraction | APIChatInputApplicationCommandInteraction | APIContextMenuInteraction | APIMessageComponentInteraction | APIModalSubmitInteraction;
|
|
4
|
+
declare const DENIAL: unique symbol;
|
|
5
|
+
/** Returned by `deny()`; tells Nubis to stop and answer with `message`. */
|
|
6
|
+
export interface Denial {
|
|
7
|
+
readonly [DENIAL]: true;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GuardContext {
|
|
11
|
+
readonly interaction: GuardedInteraction;
|
|
12
|
+
/** Discord's API, to look things up before deciding. */
|
|
13
|
+
readonly api: NubisApi;
|
|
14
|
+
/** Who used the command or component, in a server or in DMs. */
|
|
15
|
+
readonly user: APIUser;
|
|
16
|
+
/** The member, in a server; `undefined` in DMs. */
|
|
17
|
+
readonly member: APIInteractionGuildMember | undefined;
|
|
18
|
+
readonly guildId: string | undefined;
|
|
19
|
+
readonly channelId: string | undefined;
|
|
20
|
+
/** Stops here. The user sees `message`, only to them. */
|
|
21
|
+
deny(message: string): Denial;
|
|
22
|
+
}
|
|
23
|
+
export type GuardResult = Denial | undefined | void;
|
|
24
|
+
/** A check that runs before a handler. Return `deny(...)` to stop, anything else to go on. */
|
|
25
|
+
export type Guard = (context: GuardContext) => GuardResult | Promise<GuardResult>;
|
|
26
|
+
/** Defines a guard. An identity function, for types and readability. */
|
|
27
|
+
export declare function guard(check: Guard): Guard;
|
|
28
|
+
export declare function isDenial(value: unknown): value is Denial;
|
|
29
|
+
export declare function guardContext(interaction: GuardedInteraction, api?: NubisApi): GuardContext;
|
|
30
|
+
/** Runs guards in order and returns the first denial, if any. */
|
|
31
|
+
export declare function runGuards(guards: readonly Guard[], context: GuardContext): Promise<Denial | undefined>;
|
|
32
|
+
/** Only in servers, not in DMs. */
|
|
33
|
+
export declare const guildOnly: Guard;
|
|
34
|
+
/**
|
|
35
|
+
* The member needs every one of these permissions (`PermissionFlagsBits` from
|
|
36
|
+
* discord-api-types). Unlike `defaultMemberPermissions`, server admins can't
|
|
37
|
+
* override this: it's checked when the command runs.
|
|
38
|
+
*/
|
|
39
|
+
export declare function hasPermission(...permissions: bigint[]): Guard;
|
|
40
|
+
/** The bot needs these permissions in the channel, e.g. BanMembers before banning. */
|
|
41
|
+
export declare function botHasPermission(...permissions: bigint[]): Guard;
|
|
42
|
+
/** The member needs at least one of these roles. Pass ids, or a function that reads them (e.g. from config). */
|
|
43
|
+
export declare function hasRole(...roles: (string | (() => string | undefined))[]): Guard;
|
|
44
|
+
/**
|
|
45
|
+
* For components: only the person who ran the command that sent the message
|
|
46
|
+
* can use it. Without this, anyone who can see a public message can click it.
|
|
47
|
+
*/
|
|
48
|
+
export declare const invokerOnly: Guard;
|
|
49
|
+
export interface CooldownOptions {
|
|
50
|
+
readonly seconds: number;
|
|
51
|
+
/** Whose uses count together. Defaults to "user". */
|
|
52
|
+
readonly per?: "user" | "member" | "channel" | "guild" | "global";
|
|
53
|
+
/** Milliseconds since the epoch. Swappable in tests. */
|
|
54
|
+
readonly now?: () => number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Allows one use per `seconds`. Uses are counted when this guard passes, so
|
|
58
|
+
* put it after other guards: a denied permission check shouldn't use up a turn.
|
|
59
|
+
* Kept in memory, per process.
|
|
60
|
+
*/
|
|
61
|
+
export declare function cooldown(options: CooldownOptions): Guard;
|
|
62
|
+
export {};
|