@stonyx/discord 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,343 @@
1
+ # @stonyx/discord
2
+
3
+ Discord bot module for the [Stonyx framework](https://github.com/abofs/stonyx), providing plug-and-play command and event handler discovery, intent auto-derivation, and utility methods for channels, messages, and roles.
4
+
5
+ ## Highlights
6
+
7
+ * **Command auto-discovery:** Drop command files into a directory and the framework registers them automatically.
8
+ * **Event handler auto-discovery:** Same pattern for Discord gateway events.
9
+ * **Intent auto-derivation:** Required intents computed from discovered event handlers.
10
+ * **Lazy initialization:** Bot only starts if a token is configured and commands/events exist.
11
+ * **Singleton pattern:** Matches conventions of `@stonyx/sockets` and `@stonyx/events`.
12
+ * **Utility methods:** sendMessage, reply with auto-chunking, role management, and more.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm install @stonyx/discord
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### 1. Create handler directories
23
+
24
+ ```
25
+ discord-commands/ # default directory (configurable)
26
+ ping.js
27
+ discord-events/ # default directory (configurable)
28
+ message-create.js
29
+ ```
30
+
31
+ ### 2. Write a command
32
+
33
+ ```javascript
34
+ // discord-commands/ping.js
35
+ import { SlashCommandBuilder } from 'discord.js';
36
+ import { Command } from '@stonyx/discord';
37
+
38
+ export default class PingCommand extends Command {
39
+ data = new SlashCommandBuilder()
40
+ .setName('ping')
41
+ .setDescription('Replies with Pong!');
42
+
43
+ async execute(interaction) {
44
+ await interaction.reply('Pong!');
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### 3. Write an event handler
50
+
51
+ ```javascript
52
+ // discord-events/message-create.js
53
+ import { EventHandler } from '@stonyx/discord';
54
+
55
+ export default class MessageCreateHandler extends EventHandler {
56
+ static event = 'messageCreate';
57
+
58
+ handle(message) {
59
+ if (message.author.bot) return;
60
+ console.log(`${message.author.username}: ${message.content}`);
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### 4. Start the bot
66
+
67
+ With Stonyx auto-initialization (recommended):
68
+
69
+ ```bash
70
+ stonyx serve
71
+ ```
72
+
73
+ Or manually:
74
+
75
+ ```javascript
76
+ import { DiscordBot } from '@stonyx/discord';
77
+
78
+ const bot = new DiscordBot();
79
+ await bot.init();
80
+ ```
81
+
82
+ ## Command Architecture
83
+
84
+ ### How commands are discovered
85
+
86
+ On `init()`, `DiscordBot` scans the command directory using `forEachFileImport`. Each file's default export is instantiated and validated:
87
+
88
+ - Must have a `data` property (a `SlashCommandBuilder` instance)
89
+ - Must have an `execute(interaction)` method
90
+ - Missing either → logged warning, command skipped
91
+
92
+ Commands are registered by `instance.data.name` (the name set on the `SlashCommandBuilder`).
93
+
94
+ Handler filenames follow the kebab-to-camelCase convention: `ping-stats.js` → `pingStats`.
95
+
96
+ ### Command class
97
+
98
+ Each command extends `Command` and provides:
99
+
100
+ - **`data`** — a `SlashCommandBuilder` defining the command name, description, and options
101
+ - **`execute(interaction)`** — receives the Discord.js `ChatInputCommandInteraction`
102
+ - **`this._bot`** — reference to the `DiscordBot` instance (set automatically during discovery)
103
+ - **`static skipAuth`** — reserved for future auth gating (default: `false`)
104
+
105
+ ### Interaction error handling
106
+
107
+ The framework wraps `execute()` in a try/catch. On error:
108
+
109
+ 1. Logs the error via `log.error()`
110
+ 2. Sends an ephemeral "There was an error executing this command!" reply
111
+ 3. Uses `followUp` if the interaction was already replied to or deferred
112
+
113
+ ## Event Handler Architecture
114
+
115
+ ### How handlers are discovered
116
+
117
+ On `init()`, `DiscordBot` scans the event directory using `forEachFileImport`. Each file's default export is validated:
118
+
119
+ - Must have a `static event` property (the Discord.js event name)
120
+ - Missing → logged warning, handler skipped
121
+
122
+ After discovery, each handler is wired to the Discord.js client:
123
+
124
+ ```javascript
125
+ client.on(handler.constructor.event, (...args) => handler.handle(...args));
126
+ ```
127
+
128
+ ### EventHandler class
129
+
130
+ Each handler extends `EventHandler` and provides:
131
+
132
+ - **`static event`** — the Discord.js gateway event name (e.g., `'messageCreate'`, `'guildMemberAdd'`)
133
+ - **`handle(...args)`** — receives the event arguments from Discord.js
134
+ - **`this._bot`** — reference to the `DiscordBot` instance (set automatically during discovery)
135
+
136
+ ## Intent Auto-Derivation
137
+
138
+ The framework automatically computes the required intents from your registered event handlers. No manual intent configuration needed.
139
+
140
+ ### Event-to-Intent Mapping
141
+
142
+ | Event | Intents |
143
+ |-------|---------|
144
+ | `messageCreate` | `GuildMessages`, `DirectMessages`, `MessageContent` |
145
+ | `messageDelete` | `GuildMessages` |
146
+ | `messageUpdate` | `GuildMessages` |
147
+ | `guildMemberAdd` | `GuildMembers` |
148
+ | `guildMemberRemove` | `GuildMembers` |
149
+ | `inviteCreate` | `GuildInvites` |
150
+ | `inviteDelete` | `GuildInvites` |
151
+ | `voiceStateUpdate` | `GuildVoiceStates` |
152
+ | `interactionCreate` | *(none — handled by default)* |
153
+
154
+ **`Guilds` is always included** when commands are registered.
155
+
156
+ ### Intent-to-Partial Mapping
157
+
158
+ | Intent | Partials |
159
+ |--------|----------|
160
+ | `DirectMessages` | `Channel` |
161
+
162
+ ### Config overrides
163
+
164
+ Use `additionalIntents` and `additionalPartials` in config to add intents/partials that aren't covered by auto-derivation:
165
+
166
+ ```javascript
167
+ // config/environment.js (consumer app)
168
+ export default {
169
+ discord: {
170
+ additionalIntents: ['GuildPresences'],
171
+ additionalPartials: ['Message', 'Reaction'],
172
+ }
173
+ }
174
+ ```
175
+
176
+ Values are resolved against `GatewayIntentBits` and `Partials` enums from discord.js.
177
+
178
+ ## Utility Methods
179
+
180
+ All methods are on the `DiscordBot` instance (accessible via `this._bot` in commands/handlers or `new DiscordBot()`).
181
+
182
+ ### sendMessage(content, channelId, imagePath?)
183
+
184
+ Send a text message to a channel. Optionally attach an image file.
185
+
186
+ ```javascript
187
+ await bot.sendMessage('Hello world', '123456789');
188
+ await bot.sendMessage('Check this out', '123456789', './screenshot.png');
189
+ ```
190
+
191
+ ### sendFile(file, messageObject)
192
+
193
+ Edit an existing message to replace its content with a file attachment.
194
+
195
+ ```javascript
196
+ const msg = await bot.sendMessage('Processing...', channelId);
197
+ await bot.sendFile('/path/to/output.csv', msg);
198
+ ```
199
+
200
+ ### reply(interaction, content)
201
+
202
+ Reply to a slash command interaction with auto-chunking. Messages over 2000 characters are split at newline/space boundaries and sent as follow-ups.
203
+
204
+ ```javascript
205
+ await bot.reply(interaction, longContent);
206
+ ```
207
+
208
+ Handles both fresh replies and deferred/already-replied interactions automatically.
209
+
210
+ ### updateStatus(name, type?)
211
+
212
+ Set the bot's presence/activity status.
213
+
214
+ ```javascript
215
+ await bot.updateStatus('with fire', 0); // "Playing with fire"
216
+ ```
217
+
218
+ Type values: `0` = Playing, `1` = Streaming, `2` = Listening, `3` = Watching, `5` = Competing.
219
+
220
+ ### getChannelMessages(channelId, options?)
221
+
222
+ Fetch messages from a channel. Uses `maxMessagesPerRequest` as the default limit.
223
+
224
+ ```javascript
225
+ const messages = await bot.getChannelMessages('123456789');
226
+ const older = await bot.getChannelMessages('123456789', { before: '999999' });
227
+ ```
228
+
229
+ ### getChannelMessage(channelId, messageId)
230
+
231
+ Fetch a single message by ID.
232
+
233
+ ```javascript
234
+ const msg = await bot.getChannelMessage('123456789', '987654321');
235
+ ```
236
+
237
+ ### getGuild(guildId?)
238
+
239
+ Fetch a guild. Defaults to `config.discord.serverId`.
240
+
241
+ ```javascript
242
+ const guild = await bot.getGuild();
243
+ ```
244
+
245
+ ### clearChannelMessages(channelId)
246
+
247
+ Delete all fetched messages in a channel (up to `maxMessagesPerRequest`).
248
+
249
+ ```javascript
250
+ await bot.clearChannelMessages('123456789');
251
+ ```
252
+
253
+ ### giveRole(memberId, roleId)
254
+
255
+ Add a role to a guild member in the default server.
256
+
257
+ ```javascript
258
+ await bot.giveRole('111222333', '444555666');
259
+ ```
260
+
261
+ ## Configuration
262
+
263
+ Configuration is read from `stonyx/config` under `discord`:
264
+
265
+ | Option | Env Var | Default | Description |
266
+ |--------|---------|---------|-------------|
267
+ | `token` | `DISCORD_TOKEN` | `''` | Discord bot token |
268
+ | `botUserId` | `DISCORD_BOT_USER_ID` | `''` | Bot's user ID |
269
+ | `serverId` | `DISCORD_SERVER_ID` | `''` | Default guild/server ID |
270
+ | `commandDir` | `DISCORD_COMMAND_DIR` | `'./discord-commands'` | Command files directory |
271
+ | `eventDir` | `DISCORD_EVENT_DIR` | `'./discord-events'` | Event handler files directory |
272
+ | `maxMessagesPerRequest` | `DISCORD_MAX_MESSAGES_PER_REQUEST` | `100` | Max messages fetched per request |
273
+ | `additionalIntents` | — | `[]` | Extra intent names to include |
274
+ | `additionalPartials` | — | `[]` | Extra partial names to include |
275
+
276
+ ### Logging config (framework-internal)
277
+
278
+ | Key | Value | Purpose |
279
+ |-----|-------|---------|
280
+ | `log` | `DISCORD_LOG` / `false` | Enable verbose logging |
281
+ | `logColor` | `'#7289da'` | Chronicle log color for `log.discord()` |
282
+ | `logMethod` | `'discord'` | Creates `log.discord()` method |
283
+
284
+ ## API Reference
285
+
286
+ ### DiscordBot
287
+
288
+ | Method / Property | Description |
289
+ |-------------------|-------------|
290
+ | `new DiscordBot()` | Singleton constructor |
291
+ | `async init()` | Discover commands/events, derive intents, connect to Discord |
292
+ | `commands` | `Object` — registered command instances keyed by name |
293
+ | `eventHandlers` | `Array` — registered event handler instances |
294
+ | `client` | Discord.js `Client` instance (after init) |
295
+ | `ready` | `Promise` — resolves when the bot is connected and ready |
296
+ | `sendMessage(content, channelId, imagePath?)` | Send a message to a channel |
297
+ | `sendFile(file, messageObject)` | Replace a message with a file attachment |
298
+ | `reply(interaction, content)` | Reply with auto-chunking |
299
+ | `updateStatus(name, type?)` | Set bot presence |
300
+ | `getChannelMessages(channelId, options?)` | Fetch channel messages |
301
+ | `getChannelMessage(channelId, messageId)` | Fetch a single message |
302
+ | `getGuild(guildId?)` | Fetch a guild |
303
+ | `clearChannelMessages(channelId)` | Delete messages in a channel |
304
+ | `giveRole(memberId, roleId)` | Add a role to a member |
305
+ | `close()` | Destroy the Discord client and clear singleton |
306
+ | `reset()` | Close + clear all commands and handlers |
307
+
308
+ ### Command
309
+
310
+ | Property / Method | Description |
311
+ |-------------------|-------------|
312
+ | `static skipAuth = false` | Reserved for future auth gating |
313
+ | `data` | `SlashCommandBuilder` — define in subclass |
314
+ | `execute(interaction)` | Handle the slash command — define in subclass |
315
+ | `this._bot` | Reference to `DiscordBot` (set during discovery) |
316
+
317
+ ### EventHandler
318
+
319
+ | Property / Method | Description |
320
+ |-------------------|-------------|
321
+ | `static event = null` | Discord.js event name — set in subclass |
322
+ | `handle(...args)` | Handle the event — define in subclass |
323
+ | `this._bot` | Reference to `DiscordBot` (set during discovery) |
324
+
325
+ ## Example Project Structure
326
+
327
+ ```
328
+ my-app/
329
+ ├── config/
330
+ │ └── environment.js
331
+ ├── discord-commands/
332
+ │ ├── ping.js
333
+ │ └── stats.js
334
+ ├── discord-events/
335
+ │ ├── message-create.js
336
+ │ └── guild-member-add.js
337
+ ├── package.json
338
+ └── app.js
339
+ ```
340
+
341
+ ## License
342
+
343
+ Apache — do what you want, just keep attribution.
@@ -0,0 +1,23 @@
1
+ const {
2
+ DISCORD_TOKEN,
3
+ DISCORD_BOT_USER_ID,
4
+ DISCORD_SERVER_ID,
5
+ DISCORD_COMMAND_DIR,
6
+ DISCORD_EVENT_DIR,
7
+ DISCORD_MAX_MESSAGES_PER_REQUEST,
8
+ DISCORD_LOG,
9
+ } = process.env;
10
+
11
+ export default {
12
+ token: DISCORD_TOKEN ?? '',
13
+ botUserId: DISCORD_BOT_USER_ID ?? '',
14
+ serverId: DISCORD_SERVER_ID ?? '',
15
+ commandDir: DISCORD_COMMAND_DIR ?? './discord-commands',
16
+ eventDir: DISCORD_EVENT_DIR ?? './discord-events',
17
+ maxMessagesPerRequest: DISCORD_MAX_MESSAGES_PER_REQUEST ?? 100,
18
+ additionalIntents: [],
19
+ additionalPartials: [],
20
+ log: DISCORD_LOG ?? false,
21
+ logColor: '#7289da',
22
+ logMethod: 'discord',
23
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@stonyx/discord",
3
+ "keywords": [
4
+ "stonyx-async",
5
+ "stonyx-module"
6
+ ],
7
+ "version": "0.1.0",
8
+ "description": "Discord bot module for the Stonyx framework",
9
+ "main": "src/main.js",
10
+ "type": "module",
11
+ "files": [
12
+ "*"
13
+ ],
14
+ "exports": {
15
+ ".": "./src/main.js",
16
+ "./bot": "./src/bot.js",
17
+ "./command": "./src/command.js",
18
+ "./event-handler": "./src/event-handler.js",
19
+ "./message": "./src/message.js"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/abofs/stonyx-discord.git"
27
+ },
28
+ "author": "Stone Costa",
29
+ "license": "Apache-2.0",
30
+ "contributors": [
31
+ "Stone Costa <stone.costa@synamicd.com>"
32
+ ],
33
+ "bugs": {
34
+ "url": "https://github.com/abofs/stonyx-discord/issues"
35
+ },
36
+ "homepage": "https://github.com/abofs/stonyx-discord#readme",
37
+ "dependencies": {
38
+ "stonyx": "0.2.3-beta.6",
39
+ "discord.js": "^14.18.0"
40
+ },
41
+ "devDependencies": {
42
+ "@stonyx/utils": "0.2.3-beta.5",
43
+ "qunit": "^2.24.1",
44
+ "sinon": "^21.0.0"
45
+ },
46
+ "scripts": {
47
+ "test": "stonyx test"
48
+ }
49
+ }
package/src/bot.js ADDED
@@ -0,0 +1,214 @@
1
+ import config from 'stonyx/config';
2
+ import log from 'stonyx/log';
3
+ import { Client, GatewayIntentBits, AttachmentBuilder, MessageFlags } from 'discord.js';
4
+ import { forEachFileImport } from '@stonyx/utils/file';
5
+ import { deriveIntents, derivePartials } from './intents.js';
6
+ import { chunkMessage } from './message.js';
7
+
8
+ export default class DiscordBot {
9
+ commands = {};
10
+ eventHandlers = [];
11
+ ready = new Promise(resolve => { this.resolveReady = resolve; });
12
+
13
+ constructor() {
14
+ if (DiscordBot.instance) return DiscordBot.instance;
15
+ DiscordBot.instance = this;
16
+ }
17
+
18
+ async init() {
19
+ const { token } = config.discord;
20
+
21
+ if (!token) {
22
+ log.discord('No DISCORD_TOKEN configured — bot will not start');
23
+ this.resolveReady();
24
+ return;
25
+ }
26
+
27
+ await this.discoverCommands();
28
+ await this.discoverEvents();
29
+
30
+ const hasWork = Object.keys(this.commands).length > 0 || this.eventHandlers.length > 0;
31
+
32
+ if (!hasWork) {
33
+ log.discord('No discord commands or event handlers found — skipping bot initialization');
34
+ this.resolveReady();
35
+ return;
36
+ }
37
+
38
+ const intents = deriveIntents(this.eventHandlers, config.discord.additionalIntents);
39
+ const partials = derivePartials(intents, config.discord.additionalPartials);
40
+
41
+ if (Object.keys(this.commands).length > 0) {
42
+ intents.push(GatewayIntentBits.Guilds);
43
+ }
44
+
45
+ this.client = new Client({ intents: [...new Set(intents)], partials });
46
+ this.registerClientEvents();
47
+ this.client.login(token);
48
+
49
+ await this.ready;
50
+ }
51
+
52
+ registerClientEvents() {
53
+ const { client } = this;
54
+
55
+ client.on('clientReady', () => {
56
+ this.resolveReady();
57
+ log.discord('Discord Bot is Ready!');
58
+ });
59
+
60
+ client.on('interactionCreate', async interaction => {
61
+ if (!interaction.isChatInputCommand()) return;
62
+
63
+ const { commandName } = interaction;
64
+ const command = this.commands[commandName];
65
+
66
+ if (!command) {
67
+ return await interaction.reply({ content: `\`/${commandName}\` is not available`, flags: MessageFlags.Ephemeral });
68
+ }
69
+
70
+ try {
71
+ await command.execute(interaction);
72
+ } catch (error) {
73
+ log.error(error);
74
+ const reply = { content: 'There was an error executing this command!', flags: MessageFlags.Ephemeral };
75
+ if (interaction.replied || interaction.deferred) {
76
+ await interaction.followUp(reply);
77
+ } else {
78
+ await interaction.reply(reply);
79
+ }
80
+ }
81
+ });
82
+
83
+ for (const handler of this.eventHandlers) {
84
+ client.on(handler.constructor.event, (...args) => handler.handle(...args));
85
+ }
86
+ }
87
+
88
+ async discoverCommands() {
89
+ const { commandDir } = config.discord;
90
+
91
+ await forEachFileImport(commandDir, (CommandClass, { name }) => {
92
+ const instance = new CommandClass();
93
+
94
+ if (!instance.data || typeof instance.execute !== 'function') {
95
+ log.discord(`Command "${name}" is missing data or execute — skipping`);
96
+ return;
97
+ }
98
+
99
+ instance._bot = this;
100
+ this.commands[instance.data.name] = instance;
101
+ log.discord(`Loaded command: /${instance.data.name}`);
102
+ }, { ignoreAccessFailure: true });
103
+ }
104
+
105
+ async discoverEvents() {
106
+ const { eventDir } = config.discord;
107
+
108
+ await forEachFileImport(eventDir, (EventHandlerClass, { name }) => {
109
+ if (!EventHandlerClass.event) {
110
+ log.discord(`Event handler "${name}" is missing static event property — skipping`);
111
+ return;
112
+ }
113
+
114
+ const instance = new EventHandlerClass();
115
+ instance._bot = this;
116
+ this.eventHandlers.push(instance);
117
+ log.discord(`Loaded event handler: ${EventHandlerClass.event} (${name})`);
118
+ }, { ignoreAccessFailure: true });
119
+ }
120
+
121
+ async sendMessage(content, channelId, imagePath = null) {
122
+ const channel = await this.client.channels.fetch(channelId);
123
+ if (!channel) throw new Error('Invalid Channel ID');
124
+
125
+ const options = { content };
126
+ if (imagePath) {
127
+ options.files = [new AttachmentBuilder(imagePath)];
128
+ }
129
+ return await channel.send(options);
130
+ }
131
+
132
+ async sendFile(file, messageObject) {
133
+ return await messageObject.edit({
134
+ content: '',
135
+ files: [{
136
+ attachment: file,
137
+ name: file.split('/').pop()
138
+ }]
139
+ });
140
+ }
141
+
142
+ async reply(interaction, content) {
143
+ if (content.length <= 2000) {
144
+ if (interaction.deferred || interaction.replied) {
145
+ return await interaction.editReply({ content });
146
+ }
147
+ return await interaction.reply({ content });
148
+ }
149
+
150
+ const [first, ...rest] = chunkMessage('', content);
151
+
152
+ if (interaction.deferred || interaction.replied) {
153
+ await interaction.editReply({ content: first });
154
+ } else {
155
+ await interaction.reply({ content: first });
156
+ }
157
+
158
+ for (const chunk of rest) {
159
+ await interaction.followUp({ content: chunk });
160
+ }
161
+ }
162
+
163
+ async updateStatus(name, type = 0) {
164
+ await this.client.user.setPresence({
165
+ activities: [{ name, type }],
166
+ status: 'online'
167
+ });
168
+ }
169
+
170
+ async getChannelMessages(channelId, options = {}) {
171
+ const channel = await this.client.channels.fetch(channelId);
172
+ return await channel.messages.fetch({
173
+ limit: config.discord.maxMessagesPerRequest,
174
+ ...options
175
+ });
176
+ }
177
+
178
+ async getChannelMessage(channelId, messageId) {
179
+ const channel = await this.client.channels.fetch(channelId);
180
+ return await channel.messages.fetch(messageId);
181
+ }
182
+
183
+ async getGuild(guildId = config.discord.serverId) {
184
+ return await this.client.guilds.fetch(guildId);
185
+ }
186
+
187
+ async clearChannelMessages(channelId) {
188
+ const promises = [];
189
+ const messages = await this.getChannelMessages(channelId);
190
+ messages.forEach(message => promises.push(message.delete()));
191
+ return Promise.all(promises);
192
+ }
193
+
194
+ async giveRole(memberId, roleId) {
195
+ const guild = await this.getGuild();
196
+ const role = await guild.roles.fetch(roleId);
197
+ const member = await guild.members.fetch(memberId);
198
+ await member.roles.add(role);
199
+ }
200
+
201
+ close() {
202
+ if (this.client) {
203
+ this.client.destroy();
204
+ this.client = null;
205
+ }
206
+ DiscordBot.instance = null;
207
+ }
208
+
209
+ reset() {
210
+ this.close();
211
+ this.commands = {};
212
+ this.eventHandlers = [];
213
+ }
214
+ }
package/src/command.js ADDED
@@ -0,0 +1,3 @@
1
+ export default class Command {
2
+ static skipAuth = false;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default class EventHandler {
2
+ static event = null;
3
+ }
package/src/intents.js ADDED
@@ -0,0 +1,56 @@
1
+ import { GatewayIntentBits, Partials } from 'discord.js';
2
+
3
+ const EVENT_INTENT_MAP = {
4
+ messageCreate: [GatewayIntentBits.GuildMessages, GatewayIntentBits.DirectMessages, GatewayIntentBits.MessageContent],
5
+ messageDelete: [GatewayIntentBits.GuildMessages],
6
+ messageUpdate: [GatewayIntentBits.GuildMessages],
7
+ guildMemberAdd: [GatewayIntentBits.GuildMembers],
8
+ guildMemberRemove: [GatewayIntentBits.GuildMembers],
9
+ inviteCreate: [GatewayIntentBits.GuildInvites],
10
+ inviteDelete: [GatewayIntentBits.GuildInvites],
11
+ voiceStateUpdate: [GatewayIntentBits.GuildVoiceStates],
12
+ interactionCreate: [],
13
+ };
14
+
15
+ const INTENT_PARTIAL_MAP = {
16
+ [GatewayIntentBits.DirectMessages]: [Partials.Channel],
17
+ };
18
+
19
+ export function deriveIntents(eventHandlers, additionalIntents = []) {
20
+ const intents = new Set([GatewayIntentBits.Guilds]);
21
+
22
+ for (const handler of eventHandlers) {
23
+ const event = handler.constructor.event;
24
+ const required = EVENT_INTENT_MAP[event];
25
+ if (required) {
26
+ for (const intent of required) intents.add(intent);
27
+ }
28
+ }
29
+
30
+ for (const name of additionalIntents) {
31
+ const intent = GatewayIntentBits[name];
32
+ if (intent !== undefined) intents.add(intent);
33
+ }
34
+
35
+ return [...intents];
36
+ }
37
+
38
+ export function derivePartials(intents, additionalPartials = []) {
39
+ const partials = new Set();
40
+
41
+ for (const intent of intents) {
42
+ const required = INTENT_PARTIAL_MAP[intent];
43
+ if (required) {
44
+ for (const partial of required) partials.add(partial);
45
+ }
46
+ }
47
+
48
+ for (const name of additionalPartials) {
49
+ const partial = Partials[name];
50
+ if (partial !== undefined) partials.add(partial);
51
+ }
52
+
53
+ return [...partials];
54
+ }
55
+
56
+ export { EVENT_INTENT_MAP, INTENT_PARTIAL_MAP };