Skip to content

Commit 892b615

Browse files
📝 docs(en): update and expand docs for 0.2.0
- slash-commands: replace deprecated @slashopt with typed option decorators (@SlashStringOption, @SlashIntegerOption, etc.); add deprecation notice and migration snippet - modals: replace @field with @ModalTextField in overview example; add link to handlers-injection page - guards: add 'Guards on component handlers' section - @UseGuards on @ButtonHandler, select handlers, @ModalHandler via ComponentExecutionContext - logger: fix all imports (from '@spraxium/core' → '@spraxium/logger'); add standalone package intro section with install command - cli: add process lock section (spraxium.lock, --force-unlock, --no-lock flags, env vars) - context-menu: add full context menu commands guide (command/handler pattern, user vs message, guards, @defer, config reference, module registration) - webhook: add webhook overview guide (@WebhookSender, @send, WebhookService full API, SendOptions, defineWebhook config)
1 parent bd152ec commit 892b615

8 files changed

Lines changed: 652 additions & 95 deletions

File tree

en/cli/cli-dev.mdx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,46 @@ The CLI checks the following paths in order and runs the first one it finds:
129129

130130
A complete iteration from development to production follows this order: run _spraxium dev_ during active work, then switch to _spraxium build_ when preparing a release, and finally run _spraxium start_ to verify or deploy the compiled artifact. In CI, skip the dev step entirely and run build then start in sequence as your validation pipeline.
131131

132+
## Process lock
133+
134+
Spraxium writes a per-project lock file at `.spraxium/spraxium.lock` on startup and removes it on clean shutdown. If a second instance detects the lock, it exits with a conflict warning instead of silently duplicating listeners and command registrations.
135+
136+
### Flags
137+
138+
Both _spraxium dev_ and _spraxium start_ support two lock-related flags:
139+
140+
<Table>
141+
<TableHead>
142+
<TableRow>
143+
<TableCell header>Flag</TableCell>
144+
<TableCell header>Env var</TableCell>
145+
<TableCell header>Description</TableCell>
146+
</TableRow>
147+
</TableHead>
148+
<TableBody>
149+
<TableRow>
150+
<TableCell>`--force-unlock`</TableCell>
151+
<TableCell>`SPRAXIUM_FORCE_UNLOCK=1`</TableCell>
152+
<TableCell>Kills the existing bot process (and its dev-watcher launcher to prevent auto-respawn) then takes over. Terminal is released immediately after the kill.</TableCell>
153+
</TableRow>
154+
<TableRow>
155+
<TableCell>`--no-lock`</TableCell>
156+
<TableCell>`SPRAXIUM_NO_LOCK=1`</TableCell>
157+
<TableCell>Skips the lock check entirely. Use this only in setups that intentionally run concurrent instances (e.g. a testing environment running alongside production).</TableCell>
158+
</TableRow>
159+
</TableBody>
160+
</Table>
161+
162+
```bash filename="terminal"
163+
# Kill the running instance and take over
164+
spraxium dev --force-unlock
165+
166+
# Skip the lock check (concurrent instances allowed)
167+
spraxium dev --no-lock
168+
```
169+
170+
Stale locks from crashed processes are detected automatically via `process.kill(pid, 0)` and cleaned before startup, so you only see conflict warnings for genuinely running instances.
171+
132172
## Next page
133173

134174
Continue with [CLI Database](/guide/cli-database).
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
---
2+
title: Context Menu Commands
3+
description: |
4+
Context menu commands appear in Discord's right-click Apps submenu for users and messages.
5+
Learn the command-handler pattern, user vs message targeting, parameter injection, permission control, and module registration.
6+
order: 1
7+
category: Context Menu
8+
---
9+
10+
## What context menu commands are
11+
12+
Context menu commands are application commands that Discord exposes in the right-click context menu on users and messages rather than through the `/` slash picker. When a user right-clicks a server member and opens the _Apps_ submenu, any registered _user_ context menu commands appear there. When a user right-clicks a message, any registered _message_ context menu commands appear instead. Discord calls both types _application commands_, but their dispatch path and the data they carry are completely different from slash commands.
13+
14+
The key distinction from slash commands is that context menu commands have no options. The interaction itself carries the target — either the `User` and optional `GuildMember` for a user command, or the `Message` for a message command — and the handler accesses that target directly from the interaction object. There is no `@SlashOpt()` injection and no option schema to define.
15+
16+
Spraxium implements context menu commands with the same command-handler separation pattern used by slash commands. A _@ContextMenuCommand_ class declares the command metadata, and a separate _@ContextMenuCommandHandler_ class implements the handler logic. Guards, permissions, and the exception pipeline all work the same way they do for slash commands.
17+
18+
## Defining a context menu command
19+
20+
The _@ContextMenuCommand_ decorator accepts a config object with the command _name_ and _type_. The name is what appears in the Discord Apps submenu — it can contain spaces, up to 32 characters. The type is either `'user'` for user-targeted commands or `'message'` for message-targeted commands.
21+
22+
The command class itself is a pure declaration with no implementation. Think of it as the schema — it defines what gets registered on Discord's side and what metadata the handler resolver uses to wire things together.
23+
24+
<CodeTabs>
25+
<Tab label="User command" language="typescript">
26+
```typescript filename="src/modules/user-info/commands/user-info.command.ts"
27+
import { ContextMenuCommand } from '@spraxium/common';
28+
29+
@ContextMenuCommand({ name: 'User Info', type: 'user' })
30+
export class UserInfoCommand {}
31+
```
32+
</Tab>
33+
<Tab label="Message command" language="typescript">
34+
```typescript filename="src/modules/quotes/commands/quote.command.ts"
35+
import { ContextMenuCommand } from '@spraxium/common';
36+
37+
@ContextMenuCommand({ name: 'Quote', type: 'message' })
38+
export class QuoteCommand {}
39+
```
40+
</Tab>
41+
</CodeTabs>
42+
43+
## Writing the handler
44+
45+
The handler class is decorated with _@ContextMenuCommandHandler_, passing the command class as its argument. The handler must expose a `handle()` method. Use _@Ctx()_ to receive the interaction — the exact type depends on the command type.
46+
47+
For a `'user'` command, the interaction is `UserContextMenuCommandInteraction`. It exposes `interaction.targetUser` (the `User` object) and `interaction.targetMember` (the resolved `GuildMember` if the bot is in a guild, otherwise `null`).
48+
49+
For a `'message'` command, the interaction is `MessageContextMenuCommandInteraction`. It exposes `interaction.targetMessage` (the `Message` object).
50+
51+
<CodeTabs>
52+
<Tab label="User handler" language="typescript">
53+
```typescript filename="src/modules/user-info/handlers/user-info-command.handler.ts"
54+
import { ContextMenuCommandHandler, Ctx } from '@spraxium/common';
55+
import { type UserContextMenuCommandInteraction, time } from 'discord.js';
56+
import { UserInfoCommand } from '../commands/user-info.command';
57+
58+
@ContextMenuCommandHandler(UserInfoCommand)
59+
export class UserInfoHandler {
60+
async handle(@Ctx() interaction: UserContextMenuCommandInteraction): Promise<void> {
61+
const user = interaction.targetUser;
62+
const member = interaction.targetMember;
63+
64+
const lines = [
65+
`**${user.tag}** (\`${user.id}\`)`,
66+
`Account created: ${time(user.createdAt, 'R')}`,
67+
];
68+
69+
if (member && 'joinedAt' in member && member.joinedAt) {
70+
lines.push(`Joined server: ${time(member.joinedAt, 'R')}`);
71+
}
72+
73+
await interaction.reply({ content: lines.join('\n'), flags: 'Ephemeral' });
74+
}
75+
}
76+
```
77+
</Tab>
78+
<Tab label="Message handler" language="typescript">
79+
```typescript filename="src/modules/quotes/handlers/quote-command.handler.ts"
80+
import { ContextMenuCommandHandler, Ctx } from '@spraxium/common';
81+
import type { MessageContextMenuCommandInteraction } from 'discord.js';
82+
import { QuoteCommand } from '../commands/quote.command';
83+
84+
@ContextMenuCommandHandler(QuoteCommand)
85+
export class QuoteHandler {
86+
async handle(@Ctx() interaction: MessageContextMenuCommandInteraction): Promise<void> {
87+
const message = interaction.targetMessage;
88+
const content = message.content?.trim().length
89+
? message.content
90+
: '*(no text content)*';
91+
92+
const quoted = content
93+
.split('\n')
94+
.map((line) => `> ${line}`)
95+
.join('\n');
96+
97+
await interaction.reply({
98+
content: `Quoting **${message.author.tag}**:\n${quoted}`,
99+
allowedMentions: { parse: [] },
100+
});
101+
}
102+
}
103+
```
104+
</Tab>
105+
</CodeTabs>
106+
107+
## Command configuration reference
108+
109+
The _@ContextMenuCommand_ config accepts several optional fields beyond _name_ and _type_.
110+
111+
<Table>
112+
<TableHead>
113+
<TableRow>
114+
<TableCell header>Field</TableCell>
115+
<TableCell header>Type</TableCell>
116+
<TableCell header>Required</TableCell>
117+
<TableCell header>Description</TableCell>
118+
</TableRow>
119+
</TableHead>
120+
<TableBody>
121+
<TableRow>
122+
<TableCell>*name*</TableCell>
123+
<TableCell>`string`</TableCell>
124+
<TableCell>Yes</TableCell>
125+
<TableCell>Display name shown in the Apps submenu. Up to 32 characters; spaces are allowed.</TableCell>
126+
</TableRow>
127+
<TableRow>
128+
<TableCell>*type*</TableCell>
129+
<TableCell>`'user' | 'message'`</TableCell>
130+
<TableCell>Yes</TableCell>
131+
<TableCell>Whether the command appears on user right-click or message right-click.</TableCell>
132+
</TableRow>
133+
<TableRow>
134+
<TableCell>*guild*</TableCell>
135+
<TableCell>`string`</TableCell>
136+
<TableCell>No</TableCell>
137+
<TableCell>Register as a guild command instead of globally. Propagates immediately with no Discord approval delay. Useful for testing.</TableCell>
138+
</TableRow>
139+
<TableRow>
140+
<TableCell>*defaultMemberPermissions*</TableCell>
141+
<TableCell>`bigint | number | null`</TableCell>
142+
<TableCell>No</TableCell>
143+
<TableCell>Discord permission bitfield controlling who sees the command. Server admins can override this in guild settings.</TableCell>
144+
</TableRow>
145+
<TableRow>
146+
<TableCell>*dmPermission*</TableCell>
147+
<TableCell>`boolean`</TableCell>
148+
<TableCell>No</TableCell>
149+
<TableCell>Whether the command is available in DMs. Defaults to `true`.</TableCell>
150+
</TableRow>
151+
<TableRow>
152+
<TableCell>*nsfw*</TableCell>
153+
<TableCell>`boolean`</TableCell>
154+
<TableCell>No</TableCell>
155+
<TableCell>Marks the command as NSFW. Discord hides it outside age-restricted channels for non-verified users.</TableCell>
156+
</TableRow>
157+
</TableBody>
158+
</Table>
159+
160+
## Guards and permissions
161+
162+
Context menu command handlers support _@UseGuards_ exactly the same way slash command handlers do. The guard pipeline runs before the handler method, and any guard that denies the interaction short-circuits execution. The exception pipeline also works identically — throwing a _SpraxiumException_ inside the handler or a guard produces a structured Discord reply.
163+
164+
```typescript filename="src/modules/moderation/handlers/flag-message.handler.ts"
165+
import { ContextMenuCommandHandler, Ctx, UseGuards } from '@spraxium/common';
166+
import { GuildOnlyGuard } from '@spraxium/common';
167+
import { PermissionFlagsBits } from 'discord.js';
168+
import type { MessageContextMenuCommandInteraction } from 'discord.js';
169+
import { FlagMessageCommand } from '../commands/flag-message.command';
170+
171+
@ContextMenuCommandHandler(FlagMessageCommand)
172+
@UseGuards(GuildOnlyGuard)
173+
export class FlagMessageHandler {
174+
async handle(@Ctx() interaction: MessageContextMenuCommandInteraction): Promise<void> {
175+
const message = interaction.targetMessage;
176+
// ... moderation logic
177+
await interaction.reply({ content: 'Message flagged.', flags: 'Ephemeral' });
178+
}
179+
}
180+
```
181+
182+
_@Defer_ and _@AutoDefer_ are also supported. Apply them to the handler class the same way as on slash command handlers. The defer behavior is identical — the framework defers the interaction after the guard pipeline passes.
183+
184+
## Module registration
185+
186+
Both the command class and the handler class must be registered in a module. Command classes go in the _commands_ array and handler classes go in the _handlers_ array. The framework reads command metadata from the _commands_ array to build the Discord registration payload, and reads handler metadata to wire up the runtime dispatcher.
187+
188+
```typescript filename="src/modules/user-info/user-info.module.ts"
189+
import { Module } from '@spraxium/common';
190+
import { AvatarCommand } from './commands/avatar.command';
191+
import { UserInfoCommand } from './commands/user-info.command';
192+
import { AvatarHandler } from './handlers/avatar-command.handler';
193+
import { UserInfoHandler } from './handlers/user-info-command.handler';
194+
195+
@Module({
196+
commands: [UserInfoCommand, AvatarCommand],
197+
handlers: [UserInfoHandler, AvatarHandler],
198+
})
199+
export class UserContextMenuModule {}
200+
```
201+
202+
Context menu commands and slash commands can live in the same module with no conflicts. Both command types share the same registration phase and the same dispatcher infrastructure. You can co-locate a slash command, its handler, and a context menu command pointing at similar functionality all inside one module if that makes organizational sense for your bot.

en/guards/guards-registration.mdx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,34 @@ This predictable ordering means your team can always reason about which guards r
132132
confusion about where the policy is actually enforced.
133133
</Callout>
134134

135+
## Guards on component handlers
136+
137+
_@UseGuards_ works on component handlers too — `@ButtonHandler`, `@StringSelectHandler` (and all select variants), and `@ModalHandler` all run the same _GuardExecutor_ pipeline used by slash and context-menu commands.
138+
139+
```typescript filename="src/modules/vip/handlers/vip-claim-button.handler.ts"
140+
import { UseGuards, withOptions } from '@spraxium/common';
141+
import { ButtonHandler, Ctx } from '@spraxium/components';
142+
import type { ButtonInteraction } from 'discord.js';
143+
import { VipClaimButton } from '../components/vip-claim.button';
144+
import { VipRoleGuard } from '../guards/vip-role.guard';
145+
146+
@UseGuards(withOptions(VipRoleGuard, { roleId: '123456789' }))
147+
@ButtonHandler(VipClaimButton)
148+
export class VipClaimButtonHandler {
149+
async handle(@Ctx() interaction: ButtonInteraction): Promise<void> {
150+
await interaction.reply({ content: '✅ VIP perk claimed!', flags: 'Ephemeral' });
151+
}
152+
}
153+
```
154+
155+
Guards on component handlers receive the same _ExecutionContext_ interface as guards on command handlers. Any guard that calls `ctx.getMember()`, `ctx.getGuildId()`, or `ctx.getMemberPermissions()` works identically on slash commands and component handlers with no modification.
156+
157+
Global guards registered via `useGlobalGuards()` at bootstrap now fire on **all** interaction types — slash commands, context menu commands, listeners, buttons, selects, and modals. This closes the gap where a `@UseGuards` on a `@SlashCommandHandler` protected the command but not the button that the reply contained.
158+
159+
<Callout tone="info" title="Guards run before payload consumption in dynamic components">
160+
For `@DynamicButtonHandler` and `@DynamicSelectHandler`, guards execute before the payload is consumed. If a guard denies, the payload is left intact and the interaction receives the guard's denial reply.
161+
</Callout>
162+
135163
## Next page
136164

137165
Continue with [Options and Built-in Guards](/guards-options-built-ins).

en/logger/logger-overview.mdx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ order: 1
77
category: Logger
88
---
99

10+
## The `@spraxium/logger` package
11+
12+
Starting in 0.2.0, the logger has been extracted from `@spraxium/core` into its own standalone package: `@spraxium/logger`. Install it alongside the framework or any other Spraxium package that needs logging.
13+
14+
```bash
15+
pnpm add @spraxium/logger
16+
```
17+
18+
All framework packages (`@spraxium/core`, `@spraxium/components`, `@spraxium/http`, etc.) now import from `@spraxium/logger` directly. The ANSI console transport has **no color dependencies**`chalk` has been removed from the entire monorepo.
19+
1020
## What the logger solves
1121

1222
Discord bots generate a steady stream of operational information: commands executed, warnings about rate limits, errors from failed API calls, and debug traces during development. The _Logger_ class provides a unified API for emitting these messages through pluggable transports. Instead of scattering `console.log` calls across your codebase, you create logger instances with named contexts, call typed methods like _info_, _warn_, or _error_, and let the transport layer decide where each message goes.
@@ -82,8 +92,8 @@ The _debug_ level is gated behind a flag. Messages sent through _debug_ are sile
8292
You create a logger by instantiating the _Logger_ class with an optional context string. The context appears in every log line between the level and the message, making it easy to filter logs by source. A typical pattern is to create one logger per service or module.
8393

8494
```typescript filename="src/services/music.service.ts"
85-
import { Injectable } from '@spraxium/core';
86-
import { Logger } from '@spraxium/core';
95+
import { Injectable } from '@spraxium/common';
96+
import { Logger } from '@spraxium/logger';
8797

8898
@Injectable()
8999
export class MusicService {
@@ -104,7 +114,7 @@ export class MusicService {
104114
The _child_ method creates a new logger that inherits the parent transport configuration but uses a different context name. This is useful inside methods that need their own identity in the log output without creating a separate class-level logger.
105115

106116
```typescript filename="src/services/queue.service.ts"
107-
import { Logger } from '@spraxium/core';
117+
import { Logger } from '@spraxium/logger';
108118

109119
const parentLogger = new Logger('QueueService');
110120
const importLogger = parentLogger.child('QueueImport');
@@ -117,8 +127,8 @@ importLogger.info('Starting playlist import');
117127
The _extend_ method creates a named log function bound to a custom level. You specify the level name and an optional color. Once extended, the function works like any built-in method and routes through all transports. This is useful for domain-specific levels like `audit`, `payment`, or `moderation` that you want to filter separately.
118128

119129
```typescript filename="src/services/audit.service.ts"
120-
import { Injectable } from '@spraxium/core';
121-
import { Logger } from '@spraxium/core';
130+
import { Injectable } from '@spraxium/common';
131+
import { Logger } from '@spraxium/logger';
122132

123133
@Injectable()
124134
export class AuditService {
@@ -138,7 +148,7 @@ The color parameter accepts any chalk color name like `'red'`, `'greenBright'`,
138148
The _log_ method is the low-level escape hatch for emitting at any level by string name. It accepts the level, the message, and an optional `metadata` object. Unlike the named methods, _log_ lets you attach structured key-value data directly to the log entry without creating a typed function through _extend_. This is the method _CommandLogger_ uses internally to attach command execution data.
139149

140150
```typescript filename="src/services/payment.service.ts"
141-
import { Logger } from '@spraxium/core';
151+
import { Logger } from '@spraxium/logger';
142152

143153
const logger = new Logger('PaymentService');
144154

@@ -156,7 +166,7 @@ The metadata keys become available as `{{key}}` placeholders in Discord embed te
156166
When you need to print a message without any formatting, timestamps, or level prefix, use the _raw_ method. It calls the native `console.log` directly, bypassing all transports and the token masker. This is useful for printing ASCII banners, separator lines, or structured output that should not be wrapped in the standard log format.
157167

158168
```typescript filename="src/main.ts"
159-
import { Logger } from '@spraxium/core';
169+
import { Logger } from '@spraxium/logger';
160170

161171
const logger = new Logger();
162172
logger.raw('');

0 commit comments

Comments
 (0)