@stonyx/discord 0.1.0 → 0.1.1-beta.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/package.json +11 -5
- package/src/main.js +0 -5
- package/.claude/architecture.md +0 -124
- package/.claude/commands.md +0 -122
- package/.claude/configuration.md +0 -126
- package/.claude/events.md +0 -121
- package/.claude/index.md +0 -36
- package/.claude/testing.md +0 -213
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/.npmignore +0 -4
package/package.json
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.1.0",
|
|
7
|
+
"version": "0.1.1-beta.0",
|
|
8
8
|
"description": "Discord bot module for the Stonyx framework",
|
|
9
9
|
"main": "src/main.js",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
12
|
+
"src",
|
|
13
|
+
"config",
|
|
14
|
+
"LICENSE.md",
|
|
15
|
+
"README.md"
|
|
13
16
|
],
|
|
14
17
|
"exports": {
|
|
15
18
|
".": "./src/main.js",
|
|
@@ -35,13 +38,16 @@
|
|
|
35
38
|
},
|
|
36
39
|
"homepage": "https://github.com/abofs/stonyx-discord#readme",
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"stonyx": "0.2.3-beta.6",
|
|
39
41
|
"discord.js": "^14.18.0"
|
|
40
42
|
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"stonyx": ">=0.2.3-beta.4"
|
|
45
|
+
},
|
|
41
46
|
"devDependencies": {
|
|
42
|
-
"@stonyx/utils": "0.2.3-beta.
|
|
47
|
+
"@stonyx/utils": "0.2.3-beta.7",
|
|
43
48
|
"qunit": "^2.24.1",
|
|
44
|
-
"sinon": "^21.0.0"
|
|
49
|
+
"sinon": "^21.0.0",
|
|
50
|
+
"stonyx": "0.2.3-beta.10"
|
|
45
51
|
},
|
|
46
52
|
"scripts": {
|
|
47
53
|
"test": "stonyx test"
|
package/src/main.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
export { default as DiscordBot } from './bot.js';
|
|
2
|
-
export { default as Command } from './command.js';
|
|
3
|
-
export { default as EventHandler } from './event-handler.js';
|
|
4
|
-
export { chunkMessage } from './message.js';
|
|
5
|
-
|
|
6
1
|
export default class Discord {
|
|
7
2
|
constructor() {
|
|
8
3
|
if (Discord.instance) return Discord.instance;
|
package/.claude/architecture.md
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
## Module Structure
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
stonyx-discord/
|
|
7
|
-
├── config/environment.js # Default config, env var overrides
|
|
8
|
-
├── src/
|
|
9
|
-
│ ├── main.js # Discord class (Stonyx entry) + barrel exports
|
|
10
|
-
│ ├── bot.js # DiscordBot singleton — core logic
|
|
11
|
-
│ ├── command.js # Base Command class
|
|
12
|
-
│ ├── event-handler.js # Base EventHandler class
|
|
13
|
-
│ ├── intents.js # Intent/partial auto-derivation
|
|
14
|
-
│ └── message.js # Message chunking utility
|
|
15
|
-
└── test/
|
|
16
|
-
├── config/environment.js # Test config overrides
|
|
17
|
-
├── sample/ # Sample commands and event handlers
|
|
18
|
-
└── unit/ # Unit tests
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Stonyx Integration
|
|
22
|
-
|
|
23
|
-
### Auto-Initialization
|
|
24
|
-
|
|
25
|
-
The package declares `stonyx-async` + `stonyx-module` keywords. When Stonyx starts:
|
|
26
|
-
|
|
27
|
-
1. Reads `config/environment.js` and merges into `config.discord`
|
|
28
|
-
2. Registers `log.discord()` via `logColor: '#7289da'` + `logMethod: 'discord'`
|
|
29
|
-
3. Imports `src/main.js`, instantiates the default `Discord` class, calls `.init()`
|
|
30
|
-
4. `Discord.init()` creates a `DiscordBot` instance and calls `bot.init()`
|
|
31
|
-
|
|
32
|
-
### Standalone Mode (Testing)
|
|
33
|
-
|
|
34
|
-
When running `stonyx test` from within the package directory, Stonyx detects the `stonyx-` prefix in the path and transforms the config:
|
|
35
|
-
|
|
36
|
-
```javascript
|
|
37
|
-
// config/environment.js exports { token, botUserId, serverId, ... }
|
|
38
|
-
// Stonyx wraps it as: { discord: { token, botUserId, serverId, ... } }
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Test overrides from `test/config/environment.js` are merged on top.
|
|
42
|
-
|
|
43
|
-
## Singleton Pattern
|
|
44
|
-
|
|
45
|
-
Both `Discord` (entry point) and `DiscordBot` use the Stonyx singleton convention:
|
|
46
|
-
|
|
47
|
-
```javascript
|
|
48
|
-
constructor() {
|
|
49
|
-
if (DiscordBot.instance) return DiscordBot.instance;
|
|
50
|
-
DiscordBot.instance = this;
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`reset()` sets the static instance back to `null` and clears internal state (used in tests).
|
|
55
|
-
|
|
56
|
-
## Lazy Initialization Flow
|
|
57
|
-
|
|
58
|
-
`DiscordBot.init()` implements a lazy start — the bot only connects if there is work to do:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
init()
|
|
62
|
-
├── No token? → log warning, resolve ready, return (no bot started)
|
|
63
|
-
├── discoverCommands() → scan commandDir
|
|
64
|
-
├── discoverEvents() → scan eventDir
|
|
65
|
-
├── No commands AND no events? → log warning, resolve ready, return
|
|
66
|
-
└── Has work:
|
|
67
|
-
├── deriveIntents(eventHandlers, additionalIntents)
|
|
68
|
-
├── derivePartials(intents, additionalPartials)
|
|
69
|
-
├── If commands exist → add Guilds intent
|
|
70
|
-
├── Create discord.js Client with deduped intents + partials
|
|
71
|
-
├── registerClientEvents() → wire interactionCreate + event handlers
|
|
72
|
-
├── client.login(token)
|
|
73
|
-
└── await ready (resolves on 'clientReady' event)
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The `ready` property is a Promise created in the constructor. It resolves either when the bot connects or when init determines there is nothing to do.
|
|
77
|
-
|
|
78
|
-
## Handler Discovery Lifecycle
|
|
79
|
-
|
|
80
|
-
### Command Discovery
|
|
81
|
-
|
|
82
|
-
```javascript
|
|
83
|
-
await forEachFileImport(commandDir, (CommandClass, { name }) => {
|
|
84
|
-
const instance = new CommandClass();
|
|
85
|
-
// Validate: must have data (SlashCommandBuilder) and execute function
|
|
86
|
-
instance._bot = this;
|
|
87
|
-
this.commands[instance.data.name] = instance;
|
|
88
|
-
}, { ignoreAccessFailure: true });
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
- Commands are keyed by `instance.data.name` (the slash command name), not the filename
|
|
92
|
-
- `ignoreAccessFailure: true` means a missing directory is silently ignored
|
|
93
|
-
|
|
94
|
-
### Event Handler Discovery
|
|
95
|
-
|
|
96
|
-
```javascript
|
|
97
|
-
await forEachFileImport(eventDir, (EventHandlerClass, { name }) => {
|
|
98
|
-
// Validate: must have static event property
|
|
99
|
-
const instance = new EventHandlerClass();
|
|
100
|
-
instance._bot = this;
|
|
101
|
-
this.eventHandlers.push(instance);
|
|
102
|
-
}, { ignoreAccessFailure: true });
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
- Event handlers are stored in an array (not keyed), since multiple handlers could share an event name
|
|
106
|
-
- Each handler is wired to the client after discovery: `client.on(handler.constructor.event, (...args) => handler.handle(...args))`
|
|
107
|
-
|
|
108
|
-
## Client Event Wiring
|
|
109
|
-
|
|
110
|
-
`registerClientEvents()` sets up three things on the discord.js `Client`:
|
|
111
|
-
|
|
112
|
-
1. **`clientReady`** — resolves the `ready` promise and logs
|
|
113
|
-
2. **`interactionCreate`** — routes slash commands to the matching `Command.execute()`, with error handling
|
|
114
|
-
3. **Event handlers** — iterates `this.eventHandlers` and binds each to its event via `client.on()`
|
|
115
|
-
|
|
116
|
-
## Dependency Graph
|
|
117
|
-
|
|
118
|
-
| Package | Purpose |
|
|
119
|
-
|---------|---------|
|
|
120
|
-
| `discord.js` | Discord API client — `Client`, `GatewayIntentBits`, `SlashCommandBuilder`, etc. |
|
|
121
|
-
| `stonyx` | Framework core — config, logging, module lifecycle |
|
|
122
|
-
| `@stonyx/utils` (dev) | `forEachFileImport` for handler auto-discovery |
|
|
123
|
-
| `qunit` (dev) | Test framework |
|
|
124
|
-
| `sinon` (dev) | Stubs and spies for unit tests |
|
package/.claude/commands.md
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# Commands
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Commands are slash commands registered with Discord. Each command is a class that extends `Command`, defines a `data` property (a `SlashCommandBuilder`), and implements an `execute(interaction)` method. Command files live in the command directory (default: `./discord-commands`).
|
|
6
|
-
|
|
7
|
-
## Base Class
|
|
8
|
-
|
|
9
|
-
```javascript
|
|
10
|
-
// src/command.js
|
|
11
|
-
export default class Command {
|
|
12
|
-
static skipAuth = false;
|
|
13
|
-
}
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
The base class provides the `skipAuth` default and a common prototype. The `skipAuth` property is reserved for future auth gating.
|
|
17
|
-
|
|
18
|
-
## Defining a Command
|
|
19
|
-
|
|
20
|
-
```javascript
|
|
21
|
-
import { SlashCommandBuilder } from 'discord.js';
|
|
22
|
-
import { Command } from '@stonyx/discord';
|
|
23
|
-
|
|
24
|
-
export default class PingCommand extends Command {
|
|
25
|
-
data = new SlashCommandBuilder()
|
|
26
|
-
.setName('ping')
|
|
27
|
-
.setDescription('Replies with Pong!');
|
|
28
|
-
|
|
29
|
-
async execute(interaction) {
|
|
30
|
-
await interaction.reply('Pong!');
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Required properties
|
|
36
|
-
|
|
37
|
-
- **`data`** — a `SlashCommandBuilder` instance. The `name` set here is what users type as `/name`
|
|
38
|
-
- **`execute(interaction)`** — receives the Discord.js `ChatInputCommandInteraction`. Must be a function
|
|
39
|
-
|
|
40
|
-
### Available context
|
|
41
|
-
|
|
42
|
-
- **`this._bot`** — reference to the `DiscordBot` instance, set automatically during discovery. Use it to access utility methods like `this._bot.reply()`, `this._bot.sendMessage()`, etc.
|
|
43
|
-
|
|
44
|
-
## Command Discovery
|
|
45
|
-
|
|
46
|
-
On `DiscordBot.init()`, commands are discovered via `forEachFileImport`:
|
|
47
|
-
|
|
48
|
-
```javascript
|
|
49
|
-
await forEachFileImport(commandDir, (CommandClass, { name }) => {
|
|
50
|
-
const instance = new CommandClass();
|
|
51
|
-
|
|
52
|
-
if (!instance.data || typeof instance.execute !== 'function') {
|
|
53
|
-
log.discord(`Command "${name}" is missing data or execute — skipping`);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
instance._bot = this;
|
|
58
|
-
this.commands[instance.data.name] = instance;
|
|
59
|
-
log.discord(`Loaded command: /${instance.data.name}`);
|
|
60
|
-
}, { ignoreAccessFailure: true });
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Key details:
|
|
64
|
-
|
|
65
|
-
- **Registration key:** Commands are stored by `instance.data.name` (the slash command name), not the filename
|
|
66
|
-
- **Filename convention:** kebab-case files (`ping-stats.js`) are converted to camelCase (`pingStats`) by `forEachFileImport`, but the command name comes from `data.name`
|
|
67
|
-
- **Missing directory:** `ignoreAccessFailure: true` means if the command directory doesn't exist, no error is thrown
|
|
68
|
-
- **Validation:** Commands missing `data` or `execute` are logged and skipped
|
|
69
|
-
|
|
70
|
-
## SlashCommandBuilder Usage
|
|
71
|
-
|
|
72
|
-
Commands use the discord.js `SlashCommandBuilder` API:
|
|
73
|
-
|
|
74
|
-
```javascript
|
|
75
|
-
import { SlashCommandBuilder } from 'discord.js';
|
|
76
|
-
|
|
77
|
-
// Simple command
|
|
78
|
-
data = new SlashCommandBuilder()
|
|
79
|
-
.setName('ping')
|
|
80
|
-
.setDescription('Check if the bot is alive');
|
|
81
|
-
|
|
82
|
-
// Command with options
|
|
83
|
-
data = new SlashCommandBuilder()
|
|
84
|
-
.setName('roll')
|
|
85
|
-
.setDescription('Roll dice')
|
|
86
|
-
.addIntegerOption(option =>
|
|
87
|
-
option.setName('sides')
|
|
88
|
-
.setDescription('Number of sides')
|
|
89
|
-
.setRequired(true)
|
|
90
|
-
);
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Interaction Handling
|
|
94
|
-
|
|
95
|
-
When a user runs a slash command, the framework's `interactionCreate` listener:
|
|
96
|
-
|
|
97
|
-
1. Checks `interaction.isChatInputCommand()` — ignores non-command interactions
|
|
98
|
-
2. Looks up `this.commands[commandName]`
|
|
99
|
-
3. If not found → replies with ephemeral "not available" message
|
|
100
|
-
4. Calls `command.execute(interaction)` wrapped in try/catch
|
|
101
|
-
5. On error → logs via `log.error()`, sends ephemeral error reply (uses `followUp` if already replied/deferred)
|
|
102
|
-
|
|
103
|
-
## skipAuth Property
|
|
104
|
-
|
|
105
|
-
`static skipAuth = false` is defined on the base `Command` class. This property is reserved for future use — it does not currently gate execution. All commands are executed if they are registered.
|
|
106
|
-
|
|
107
|
-
## Using Bot Utilities in Commands
|
|
108
|
-
|
|
109
|
-
Access the bot instance via `this._bot`:
|
|
110
|
-
|
|
111
|
-
```javascript
|
|
112
|
-
export default class StatusCommand extends Command {
|
|
113
|
-
data = new SlashCommandBuilder()
|
|
114
|
-
.setName('status')
|
|
115
|
-
.setDescription('Show bot status');
|
|
116
|
-
|
|
117
|
-
async execute(interaction) {
|
|
118
|
-
const guild = await this._bot.getGuild();
|
|
119
|
-
await this._bot.reply(interaction, `Serving ${guild.memberCount} members`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
package/.claude/configuration.md
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# Configuration
|
|
2
|
-
|
|
3
|
-
## How Config Loads
|
|
4
|
-
|
|
5
|
-
The package provides `config/environment.js` with defaults. Stonyx merges this into `config.discord`:
|
|
6
|
-
|
|
7
|
-
1. Stonyx reads `config/environment.js` (the raw export)
|
|
8
|
-
2. Wraps it under the `discord` key (derived from package name `@stonyx/discord` -> `discord`)
|
|
9
|
-
3. Merges any user overrides from the consumer app's environment config
|
|
10
|
-
4. In test mode, merges `test/config/environment.js` on top
|
|
11
|
-
|
|
12
|
-
Access in code: `import config from 'stonyx/config'` -> `config.discord.token`, etc.
|
|
13
|
-
|
|
14
|
-
## Config Options
|
|
15
|
-
|
|
16
|
-
| Key | Env Var | Default | Type | Description |
|
|
17
|
-
|-----|---------|---------|------|-------------|
|
|
18
|
-
| `token` | `DISCORD_TOKEN` | `''` | String | Discord bot token. If empty, bot will not start |
|
|
19
|
-
| `botUserId` | `DISCORD_BOT_USER_ID` | `''` | String | The bot's Discord user ID |
|
|
20
|
-
| `serverId` | `DISCORD_SERVER_ID` | `''` | String | Default guild/server ID (used by `getGuild()`, `giveRole()`) |
|
|
21
|
-
| `commandDir` | `DISCORD_COMMAND_DIR` | `'./discord-commands'` | String | Path to command files directory |
|
|
22
|
-
| `eventDir` | `DISCORD_EVENT_DIR` | `'./discord-events'` | String | Path to event handler files directory |
|
|
23
|
-
| `maxMessagesPerRequest` | `DISCORD_MAX_MESSAGES_PER_REQUEST` | `100` | Number | Max messages fetched per `getChannelMessages()` call |
|
|
24
|
-
| `additionalIntents` | — | `[]` | Array | Extra intent names to include beyond auto-derived ones |
|
|
25
|
-
| `additionalPartials` | — | `[]` | Array | Extra partial names to include beyond auto-derived ones |
|
|
26
|
-
|
|
27
|
-
### Logging config (framework-internal)
|
|
28
|
-
|
|
29
|
-
| Key | Value | Purpose |
|
|
30
|
-
|-----|-------|---------|
|
|
31
|
-
| `log` | `DISCORD_LOG` / `false` | Enable verbose logging |
|
|
32
|
-
| `logColor` | `'#7289da'` | Chronicle log color for `log.discord()` |
|
|
33
|
-
| `logMethod` | `'discord'` | Creates `log.discord()` method |
|
|
34
|
-
|
|
35
|
-
## Default Config File
|
|
36
|
-
|
|
37
|
-
```javascript
|
|
38
|
-
// config/environment.js
|
|
39
|
-
const {
|
|
40
|
-
DISCORD_TOKEN,
|
|
41
|
-
DISCORD_BOT_USER_ID,
|
|
42
|
-
DISCORD_SERVER_ID,
|
|
43
|
-
DISCORD_COMMAND_DIR,
|
|
44
|
-
DISCORD_EVENT_DIR,
|
|
45
|
-
DISCORD_MAX_MESSAGES_PER_REQUEST,
|
|
46
|
-
DISCORD_LOG,
|
|
47
|
-
} = process.env;
|
|
48
|
-
|
|
49
|
-
export default {
|
|
50
|
-
token: DISCORD_TOKEN ?? '',
|
|
51
|
-
botUserId: DISCORD_BOT_USER_ID ?? '',
|
|
52
|
-
serverId: DISCORD_SERVER_ID ?? '',
|
|
53
|
-
commandDir: DISCORD_COMMAND_DIR ?? './discord-commands',
|
|
54
|
-
eventDir: DISCORD_EVENT_DIR ?? './discord-events',
|
|
55
|
-
maxMessagesPerRequest: DISCORD_MAX_MESSAGES_PER_REQUEST ?? 100,
|
|
56
|
-
additionalIntents: [],
|
|
57
|
-
additionalPartials: [],
|
|
58
|
-
log: DISCORD_LOG ?? false,
|
|
59
|
-
logColor: '#7289da',
|
|
60
|
-
logMethod: 'discord',
|
|
61
|
-
};
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Intent Auto-Derivation Details
|
|
65
|
-
|
|
66
|
-
Intents are computed automatically from registered event handlers. The process:
|
|
67
|
-
|
|
68
|
-
1. Start with an empty set
|
|
69
|
-
2. For each event handler, look up the event name in `EVENT_INTENT_MAP` (in `src/intents.js`)
|
|
70
|
-
3. Add all mapped intents to the set
|
|
71
|
-
4. Add any intents from `config.discord.additionalIntents` (resolved against `GatewayIntentBits` enum)
|
|
72
|
-
5. If commands are registered, add `GatewayIntentBits.Guilds`
|
|
73
|
-
6. Deduplicate and pass to the discord.js `Client`
|
|
74
|
-
|
|
75
|
-
### additionalIntents
|
|
76
|
-
|
|
77
|
-
An array of `GatewayIntentBits` key names (strings). These are resolved at runtime:
|
|
78
|
-
|
|
79
|
-
```javascript
|
|
80
|
-
for (const name of additionalIntents) {
|
|
81
|
-
const intent = GatewayIntentBits[name];
|
|
82
|
-
if (intent !== undefined) intents.add(intent);
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Valid values include: `Guilds`, `GuildMembers`, `GuildMessages`, `GuildPresences`, `DirectMessages`, `MessageContent`, `GuildVoiceStates`, `GuildInvites`, etc.
|
|
87
|
-
|
|
88
|
-
### additionalPartials
|
|
89
|
-
|
|
90
|
-
Same pattern — an array of `Partials` key names (strings). Resolved against the discord.js `Partials` enum.
|
|
91
|
-
|
|
92
|
-
Valid values include: `Channel`, `Message`, `Reaction`, `User`, `GuildMember`, `ThreadMember`, etc.
|
|
93
|
-
|
|
94
|
-
## Consumer Override Example
|
|
95
|
-
|
|
96
|
-
In a consumer app's `config/environment.js`:
|
|
97
|
-
|
|
98
|
-
```javascript
|
|
99
|
-
export default {
|
|
100
|
-
discord: {
|
|
101
|
-
commandDir: './src/commands',
|
|
102
|
-
eventDir: './src/events',
|
|
103
|
-
additionalIntents: ['GuildPresences'],
|
|
104
|
-
additionalPartials: ['Message'],
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Only the keys you specify are overridden — the rest keep their defaults via Stonyx's `mergeObject`.
|
|
110
|
-
|
|
111
|
-
## Test Config Override
|
|
112
|
-
|
|
113
|
-
```javascript
|
|
114
|
-
// test/config/environment.js
|
|
115
|
-
export default {
|
|
116
|
-
discord: {
|
|
117
|
-
commandDir: './test/sample/discord-commands',
|
|
118
|
-
eventDir: './test/sample/discord-events',
|
|
119
|
-
token: 'test-token',
|
|
120
|
-
serverId: 'test-server',
|
|
121
|
-
botUserId: 'test-bot',
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Note: Test overrides use the namespaced key (`discord: { ... }`) because they're merged after Stonyx has already namespaced the config.
|
package/.claude/events.md
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# Event Handlers
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Event handlers listen to Discord gateway events. Each handler is a class that extends `EventHandler`, sets a `static event` property, and implements a `handle(...args)` method. Handler files live in the event directory (default: `./discord-events`).
|
|
6
|
-
|
|
7
|
-
## Base Class
|
|
8
|
-
|
|
9
|
-
```javascript
|
|
10
|
-
// src/event-handler.js
|
|
11
|
-
export default class EventHandler {
|
|
12
|
-
static event = null;
|
|
13
|
-
}
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
The base class provides the `event` default (`null`) and a common prototype.
|
|
17
|
-
|
|
18
|
-
## Defining an Event Handler
|
|
19
|
-
|
|
20
|
-
```javascript
|
|
21
|
-
import { EventHandler } from '@stonyx/discord';
|
|
22
|
-
|
|
23
|
-
export default class MessageCreateHandler extends EventHandler {
|
|
24
|
-
static event = 'messageCreate';
|
|
25
|
-
|
|
26
|
-
handle(message) {
|
|
27
|
-
if (message.author.bot) return;
|
|
28
|
-
console.log(`${message.author.username}: ${message.content}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Required properties
|
|
34
|
-
|
|
35
|
-
- **`static event`** — the Discord.js gateway event name (e.g., `'messageCreate'`, `'guildMemberAdd'`, `'voiceStateUpdate'`). Must be set on the class, not the instance
|
|
36
|
-
- **`handle(...args)`** — receives the event arguments from Discord.js. The arguments depend on the event type
|
|
37
|
-
|
|
38
|
-
### Available context
|
|
39
|
-
|
|
40
|
-
- **`this._bot`** — reference to the `DiscordBot` instance, set automatically during discovery
|
|
41
|
-
|
|
42
|
-
## Event Handler Discovery
|
|
43
|
-
|
|
44
|
-
On `DiscordBot.init()`, event handlers are discovered via `forEachFileImport`:
|
|
45
|
-
|
|
46
|
-
```javascript
|
|
47
|
-
await forEachFileImport(eventDir, (EventHandlerClass, { name }) => {
|
|
48
|
-
if (!EventHandlerClass.event) {
|
|
49
|
-
log.discord(`Event handler "${name}" is missing static event property — skipping`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const instance = new EventHandlerClass();
|
|
54
|
-
instance._bot = this;
|
|
55
|
-
this.eventHandlers.push(instance);
|
|
56
|
-
log.discord(`Loaded event handler: ${EventHandlerClass.event} (${name})`);
|
|
57
|
-
}, { ignoreAccessFailure: true });
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Key details:
|
|
61
|
-
|
|
62
|
-
- **Storage:** Event handlers are stored in an array (`this.eventHandlers`), not keyed by name. Multiple handlers can listen to the same event
|
|
63
|
-
- **Filename convention:** kebab-case files (`message-create.js`) are converted to camelCase by `forEachFileImport`
|
|
64
|
-
- **Missing directory:** `ignoreAccessFailure: true` means if the event directory doesn't exist, no error is thrown
|
|
65
|
-
- **Validation:** Handlers missing `static event` are logged and skipped
|
|
66
|
-
|
|
67
|
-
## Event Wiring
|
|
68
|
-
|
|
69
|
-
After discovery and client creation, each handler is bound to its event:
|
|
70
|
-
|
|
71
|
-
```javascript
|
|
72
|
-
for (const handler of this.eventHandlers) {
|
|
73
|
-
client.on(handler.constructor.event, (...args) => handler.handle(...args));
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
This uses `client.on()` (not `once`), so handlers fire on every occurrence of the event.
|
|
78
|
-
|
|
79
|
-
## Event-to-Intent Mapping
|
|
80
|
-
|
|
81
|
-
The framework automatically derives the required intents from registered event handlers. The mapping is defined in `src/intents.js`:
|
|
82
|
-
|
|
83
|
-
| Event | Required Intents |
|
|
84
|
-
|-------|-----------------|
|
|
85
|
-
| `messageCreate` | `GuildMessages`, `DirectMessages`, `MessageContent` |
|
|
86
|
-
| `messageDelete` | `GuildMessages` |
|
|
87
|
-
| `messageUpdate` | `GuildMessages` |
|
|
88
|
-
| `guildMemberAdd` | `GuildMembers` |
|
|
89
|
-
| `guildMemberRemove` | `GuildMembers` |
|
|
90
|
-
| `inviteCreate` | `GuildInvites` |
|
|
91
|
-
| `inviteDelete` | `GuildInvites` |
|
|
92
|
-
| `voiceStateUpdate` | `GuildVoiceStates` |
|
|
93
|
-
| `interactionCreate` | *(none)* |
|
|
94
|
-
|
|
95
|
-
Events not in this map do not add any automatic intents. Use `additionalIntents` in config if you need intents for unmapped events.
|
|
96
|
-
|
|
97
|
-
### Intent-to-Partial Mapping
|
|
98
|
-
|
|
99
|
-
Some intents require partials to function correctly:
|
|
100
|
-
|
|
101
|
-
| Intent | Required Partials |
|
|
102
|
-
|--------|------------------|
|
|
103
|
-
| `DirectMessages` | `Channel` |
|
|
104
|
-
|
|
105
|
-
Additional partials can be added via `additionalPartials` in config.
|
|
106
|
-
|
|
107
|
-
## Using Bot Utilities in Handlers
|
|
108
|
-
|
|
109
|
-
```javascript
|
|
110
|
-
import { EventHandler } from '@stonyx/discord';
|
|
111
|
-
|
|
112
|
-
export default class GuildMemberAddHandler extends EventHandler {
|
|
113
|
-
static event = 'guildMemberAdd';
|
|
114
|
-
|
|
115
|
-
async handle(member) {
|
|
116
|
-
const welcomeChannelId = '123456789';
|
|
117
|
-
await this._bot.sendMessage(`Welcome ${member.user.username}!`, welcomeChannelId);
|
|
118
|
-
await this._bot.giveRole(member.id, 'new-member-role-id');
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
package/.claude/index.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# @stonyx/discord — Agent Documentation Index
|
|
2
|
-
|
|
3
|
-
Comprehensive reference for AI agents working on the `@stonyx/discord` package. Start here, then drill into specific docs as needed.
|
|
4
|
-
|
|
5
|
-
## Quick Orientation
|
|
6
|
-
|
|
7
|
-
`@stonyx/discord` is a Stonyx framework module providing a Discord bot with command and event handler auto-discovery, intent auto-derivation, and channel/message/role utilities. It follows the same singleton and module conventions as `@stonyx/sockets` and `@stonyx/events`.
|
|
8
|
-
|
|
9
|
-
## Documentation
|
|
10
|
-
|
|
11
|
-
- [architecture.md](./architecture.md) — Module structure, singleton lifecycle, lazy init flow, Stonyx integration, handler discovery lifecycle
|
|
12
|
-
- [commands.md](./commands.md) — Command class API, discovery via forEachFileImport, SlashCommandBuilder usage, skipAuth
|
|
13
|
-
- [events.md](./events.md) — EventHandler class API, discovery, static event property, event-to-intent mapping
|
|
14
|
-
- [configuration.md](./configuration.md) — All config options, env vars, defaults, intent auto-derivation details
|
|
15
|
-
- [testing.md](./testing.md) — Test structure, discord.js mocking patterns, running tests, QUnit patterns
|
|
16
|
-
|
|
17
|
-
## Key Files
|
|
18
|
-
|
|
19
|
-
| File | Purpose |
|
|
20
|
-
|------|---------|
|
|
21
|
-
| `src/main.js` | Entry point — `Discord` default class (Stonyx auto-init) + barrel exports |
|
|
22
|
-
| `src/bot.js` | `DiscordBot` — singleton, command/event discovery, Discord client, utility methods |
|
|
23
|
-
| `src/command.js` | `Command` base class (skipAuth flag) |
|
|
24
|
-
| `src/event-handler.js` | `EventHandler` base class (static event property) |
|
|
25
|
-
| `src/intents.js` | Intent auto-derivation — EVENT_INTENT_MAP, deriveIntents, derivePartials |
|
|
26
|
-
| `src/message.js` | Message chunking utility — chunkMessage, splitAtBoundary |
|
|
27
|
-
| `config/environment.js` | Default config with env var overrides |
|
|
28
|
-
|
|
29
|
-
## Conventions
|
|
30
|
-
|
|
31
|
-
- **Singleton pattern:** `if (DiscordBot.instance) return DiscordBot.instance;` in constructor
|
|
32
|
-
- **Stonyx module keywords:** `stonyx-async` + `stonyx-module` in package.json
|
|
33
|
-
- **Config namespace:** `config.discord` (derived from package name `@stonyx/discord` → `discord`)
|
|
34
|
-
- **Logging:** `log.discord()` via `logColor: '#7289da'` + `logMethod: 'discord'` in config
|
|
35
|
-
- **Handler discovery:** `forEachFileImport` from `@stonyx/utils/file`, kebab-to-camelCase naming
|
|
36
|
-
- **Test runner:** `stonyx test` (not plain `qunit`) — bootstraps Stonyx before running tests
|
package/.claude/testing.md
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
# Testing
|
|
2
|
-
|
|
3
|
-
## Running Tests
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
# From the stonyx-discord directory
|
|
7
|
-
npx stonyx test
|
|
8
|
-
|
|
9
|
-
# Or via pnpm
|
|
10
|
-
pnpm test
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
**Important:** Use `stonyx test`, not plain `qunit`. The Stonyx test runner bootstraps the framework (config, logging, module init) before running QUnit. Without it, `stonyx/config` and `log.discord()` won't be available.
|
|
14
|
-
|
|
15
|
-
## Test Structure
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
test/
|
|
19
|
-
├── config/
|
|
20
|
-
│ └── environment.js # Test-specific config overrides
|
|
21
|
-
├── sample/
|
|
22
|
-
│ ├── discord-commands/ # Sample command files for testing
|
|
23
|
-
│ └── discord-events/ # Sample event handler files for testing
|
|
24
|
-
└── unit/ # Unit tests
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Test Config
|
|
28
|
-
|
|
29
|
-
```javascript
|
|
30
|
-
// test/config/environment.js
|
|
31
|
-
export default {
|
|
32
|
-
discord: {
|
|
33
|
-
commandDir: './test/sample/discord-commands',
|
|
34
|
-
eventDir: './test/sample/discord-events',
|
|
35
|
-
token: 'test-token',
|
|
36
|
-
serverId: 'test-server',
|
|
37
|
-
botUserId: 'test-bot',
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Discord.js Mocking Patterns
|
|
43
|
-
|
|
44
|
-
Since `DiscordBot` creates a real `discord.js` Client that connects to the Discord API, unit tests must stub or mock discord.js to avoid network calls.
|
|
45
|
-
|
|
46
|
-
### Stubbing the Client constructor
|
|
47
|
-
|
|
48
|
-
Use `sinon` to stub the discord.js `Client`:
|
|
49
|
-
|
|
50
|
-
```javascript
|
|
51
|
-
import sinon from 'sinon';
|
|
52
|
-
import { Client } from 'discord.js';
|
|
53
|
-
|
|
54
|
-
// Stub Client to prevent actual connections
|
|
55
|
-
const fakeClient = {
|
|
56
|
-
on: sinon.stub(),
|
|
57
|
-
login: sinon.stub().resolves(),
|
|
58
|
-
user: { setPresence: sinon.stub() },
|
|
59
|
-
channels: { fetch: sinon.stub() },
|
|
60
|
-
guilds: { fetch: sinon.stub() },
|
|
61
|
-
destroy: sinon.stub(),
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
sinon.stub(Client.prototype, 'constructor').returns(fakeClient);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Mocking interactions
|
|
68
|
-
|
|
69
|
-
For testing commands, create mock interaction objects:
|
|
70
|
-
|
|
71
|
-
```javascript
|
|
72
|
-
const mockInteraction = {
|
|
73
|
-
isChatInputCommand: () => true,
|
|
74
|
-
commandName: 'ping',
|
|
75
|
-
replied: false,
|
|
76
|
-
deferred: false,
|
|
77
|
-
reply: sinon.stub().resolves(),
|
|
78
|
-
editReply: sinon.stub().resolves(),
|
|
79
|
-
followUp: sinon.stub().resolves(),
|
|
80
|
-
options: {
|
|
81
|
-
getString: sinon.stub(),
|
|
82
|
-
getInteger: sinon.stub(),
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Mocking channels and messages
|
|
88
|
-
|
|
89
|
-
```javascript
|
|
90
|
-
const mockMessage = {
|
|
91
|
-
author: { bot: false, username: 'testuser' },
|
|
92
|
-
content: 'hello',
|
|
93
|
-
delete: sinon.stub().resolves(),
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const mockChannel = {
|
|
97
|
-
send: sinon.stub().resolves(mockMessage),
|
|
98
|
-
messages: {
|
|
99
|
-
fetch: sinon.stub().resolves(new Map([['1', mockMessage]])),
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Sample Handlers
|
|
105
|
-
|
|
106
|
-
Place sample commands and event handlers in `test/sample/` for discovery during tests:
|
|
107
|
-
|
|
108
|
-
### Sample command
|
|
109
|
-
|
|
110
|
-
```javascript
|
|
111
|
-
// test/sample/discord-commands/ping.js
|
|
112
|
-
import { SlashCommandBuilder } from 'discord.js';
|
|
113
|
-
import Command from '../../../src/command.js';
|
|
114
|
-
|
|
115
|
-
export default class PingCommand extends Command {
|
|
116
|
-
data = new SlashCommandBuilder()
|
|
117
|
-
.setName('ping')
|
|
118
|
-
.setDescription('Test ping command');
|
|
119
|
-
|
|
120
|
-
async execute(interaction) {
|
|
121
|
-
await interaction.reply('Pong!');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Sample event handler
|
|
127
|
-
|
|
128
|
-
```javascript
|
|
129
|
-
// test/sample/discord-events/message-create.js
|
|
130
|
-
import EventHandler from '../../../src/event-handler.js';
|
|
131
|
-
|
|
132
|
-
export default class MessageCreateHandler extends EventHandler {
|
|
133
|
-
static event = 'messageCreate';
|
|
134
|
-
|
|
135
|
-
handle(message) {
|
|
136
|
-
this._bot.lastMessage = message;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## Writing Unit Tests
|
|
142
|
-
|
|
143
|
-
Unit tests do NOT connect to Discord. They test class behavior directly:
|
|
144
|
-
|
|
145
|
-
```javascript
|
|
146
|
-
import QUnit from 'qunit';
|
|
147
|
-
import DiscordBot from '../../src/bot.js';
|
|
148
|
-
|
|
149
|
-
const { module, test } = QUnit;
|
|
150
|
-
|
|
151
|
-
module('[Unit] DiscordBot', function (hooks) {
|
|
152
|
-
hooks.afterEach(function () {
|
|
153
|
-
const bot = DiscordBot.instance;
|
|
154
|
-
if (bot) bot.reset();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('Singleton pattern', function (assert) {
|
|
158
|
-
const b1 = new DiscordBot();
|
|
159
|
-
const b2 = new DiscordBot();
|
|
160
|
-
assert.strictEqual(b1, b2);
|
|
161
|
-
b1.reset();
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Key patterns:
|
|
167
|
-
|
|
168
|
-
- Always call `reset()` in `afterEach` to clear the singleton
|
|
169
|
-
- Use `sinon` for stubs/spies when mocking discord.js
|
|
170
|
-
- Restore sinon in `afterEach` with `sinon.restore()`
|
|
171
|
-
- Test command discovery separately from client connection
|
|
172
|
-
|
|
173
|
-
## Testing Intent Derivation
|
|
174
|
-
|
|
175
|
-
The `deriveIntents` and `derivePartials` functions are pure and testable without mocking:
|
|
176
|
-
|
|
177
|
-
```javascript
|
|
178
|
-
import { deriveIntents, derivePartials } from '../../src/intents.js';
|
|
179
|
-
|
|
180
|
-
test('derives intents from event handlers', function (assert) {
|
|
181
|
-
const handlers = [
|
|
182
|
-
{ constructor: { event: 'messageCreate' } },
|
|
183
|
-
{ constructor: { event: 'guildMemberAdd' } },
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
const intents = deriveIntents(handlers);
|
|
187
|
-
assert.ok(intents.includes(GatewayIntentBits.GuildMessages));
|
|
188
|
-
assert.ok(intents.includes(GatewayIntentBits.GuildMembers));
|
|
189
|
-
assert.ok(intents.includes(GatewayIntentBits.Guilds)); // always included
|
|
190
|
-
});
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## Testing Message Chunking
|
|
194
|
-
|
|
195
|
-
The `chunkMessage` function is pure and testable:
|
|
196
|
-
|
|
197
|
-
```javascript
|
|
198
|
-
import { chunkMessage } from '../../src/message.js';
|
|
199
|
-
|
|
200
|
-
test('chunks long messages', function (assert) {
|
|
201
|
-
const body = 'a'.repeat(3000);
|
|
202
|
-
const chunks = chunkMessage('', body);
|
|
203
|
-
assert.ok(chunks.length > 1);
|
|
204
|
-
assert.ok(chunks.every(c => c.length <= 2000));
|
|
205
|
-
});
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
## Common Gotchas
|
|
209
|
-
|
|
210
|
-
- **Process hangs after tests:** Usually caused by an unclosed discord.js Client. Ensure `reset()` is called for all bot instances.
|
|
211
|
-
- **`log.discord is not a function`:** Running `qunit` directly instead of `stonyx test`. The Stonyx bootstrap is required.
|
|
212
|
-
- **`moduleClass is not a constructor`:** The `src/main.js` default export must be a class. The `Discord` class serves as the Stonyx auto-init entry point.
|
|
213
|
-
- **Real API calls in tests:** Always stub `Client.prototype` methods or the `login()` call to prevent actual Discord connections during tests.
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: [dev, main]
|
|
6
|
-
|
|
7
|
-
concurrency:
|
|
8
|
-
group: ci-${{ github.head_ref || github.ref }}
|
|
9
|
-
cancel-in-progress: true
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
test:
|
|
16
|
-
uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
repository_dispatch:
|
|
5
|
-
types: [cascade-publish]
|
|
6
|
-
workflow_dispatch:
|
|
7
|
-
inputs:
|
|
8
|
-
version-type:
|
|
9
|
-
description: 'Version type'
|
|
10
|
-
required: true
|
|
11
|
-
type: choice
|
|
12
|
-
options:
|
|
13
|
-
- patch
|
|
14
|
-
- minor
|
|
15
|
-
- major
|
|
16
|
-
custom-version:
|
|
17
|
-
description: 'Custom version (optional, overrides version-type)'
|
|
18
|
-
required: false
|
|
19
|
-
type: string
|
|
20
|
-
pull_request:
|
|
21
|
-
types: [opened, synchronize, reopened]
|
|
22
|
-
branches: [main]
|
|
23
|
-
push:
|
|
24
|
-
branches: [main]
|
|
25
|
-
|
|
26
|
-
concurrency:
|
|
27
|
-
group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
|
|
28
|
-
cancel-in-progress: false
|
|
29
|
-
|
|
30
|
-
permissions:
|
|
31
|
-
contents: write
|
|
32
|
-
id-token: write
|
|
33
|
-
pull-requests: write
|
|
34
|
-
|
|
35
|
-
jobs:
|
|
36
|
-
publish:
|
|
37
|
-
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
38
|
-
uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
|
|
39
|
-
with:
|
|
40
|
-
version-type: ${{ github.event.inputs.version-type }}
|
|
41
|
-
custom-version: ${{ github.event.inputs.custom-version }}
|
|
42
|
-
cascade-source: ${{ github.event.client_payload.source_package || '' }}
|
|
43
|
-
secrets: inherit
|
|
44
|
-
|
|
45
|
-
cascade:
|
|
46
|
-
needs: publish
|
|
47
|
-
uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
|
|
48
|
-
with:
|
|
49
|
-
package-name: ${{ needs.publish.outputs.package-name }}
|
|
50
|
-
published-version: ${{ needs.publish.outputs.published-version }}
|
|
51
|
-
secrets: inherit
|
package/.npmignore
DELETED