@stonyx/discord 0.1.1-beta.5 → 0.1.1-beta.50

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 CHANGED
@@ -1,3 +1,7 @@
1
+ [![CI](https://github.com/abofs/stonyx-discord/actions/workflows/ci.yml/badge.svg)](https://github.com/abofs/stonyx-discord/actions/workflows/ci.yml)
2
+ [![npm version](https://img.shields.io/npm/v/@stonyx/discord.svg)](https://www.npmjs.com/package/@stonyx/discord)
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+
1
5
  # @stonyx/discord
2
6
 
3
7
  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.
@@ -64,20 +68,13 @@ export default class MessageCreateHandler extends EventHandler {
64
68
 
65
69
  ### 4. Start the bot
66
70
 
67
- With Stonyx auto-initialization (recommended):
71
+ Stonyx auto-initializes the bot when your app boots — no manual wiring needed:
68
72
 
69
73
  ```bash
70
74
  stonyx serve
71
75
  ```
72
76
 
73
- Or manually:
74
-
75
- ```javascript
76
- import { DiscordBot } from '@stonyx/discord';
77
-
78
- const bot = new DiscordBot();
79
- await bot.init();
80
- ```
77
+ `Discord.init()` (called by the Stonyx module loader) awaits `new DiscordBot().init()`, which discovers commands/events, derives intents, and connects to the gateway. The lazy-init guards still apply: if `DISCORD_TOKEN` is unset or no commands/events exist, the bot skips login.
81
78
 
82
79
  ## Command Architecture
83
80
 
package/dist/base.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { SlashCommandBuilder, MessageFlags, PermissionFlagsBits, GatewayIntentBits, AttachmentBuilder, Client, Partials } from 'discord.js';
package/dist/base.js ADDED
@@ -0,0 +1 @@
1
+ export { SlashCommandBuilder, MessageFlags, PermissionFlagsBits, GatewayIntentBits, AttachmentBuilder, Client, Partials } from 'discord.js';
package/dist/bot.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { Client } from 'discord.js';
2
+ import type { ChatInputCommandInteraction, Message } from 'discord.js';
3
+ interface CommandInstance {
4
+ data?: {
5
+ name: string;
6
+ };
7
+ execute?: (interaction: ChatInputCommandInteraction) => Promise<void>;
8
+ _bot?: DiscordBot;
9
+ [key: string]: unknown;
10
+ }
11
+ interface EventHandlerInstance {
12
+ handle?: (...args: unknown[]) => void | Promise<void>;
13
+ _bot?: DiscordBot;
14
+ constructor: {
15
+ event: string | null;
16
+ [key: string]: unknown;
17
+ };
18
+ [key: string]: unknown;
19
+ }
20
+ export default class DiscordBot {
21
+ static instance: DiscordBot | null;
22
+ commands: Record<string, CommandInstance>;
23
+ eventHandlers: EventHandlerInstance[];
24
+ resolveReady: () => void;
25
+ ready: Promise<void>;
26
+ client: Client | null;
27
+ constructor();
28
+ init(): Promise<void>;
29
+ registerClientEvents(): void;
30
+ discoverCommands(): Promise<void>;
31
+ discoverEvents(): Promise<void>;
32
+ sendMessage(content: string, channelId: string, imagePath?: string | null): Promise<Message>;
33
+ sendFile(file: string, messageObject: Message): Promise<Message>;
34
+ reply(interaction: ChatInputCommandInteraction, content: string): Promise<void>;
35
+ updateStatus(name: string, type?: number): Promise<void>;
36
+ getChannelMessages(channelId: string, options?: Record<string, unknown>): Promise<unknown>;
37
+ getChannelMessage(channelId: string, messageId: string): Promise<unknown>;
38
+ getGuild(guildId?: string): Promise<unknown>;
39
+ clearChannelMessages(channelId: string): Promise<unknown[]>;
40
+ giveRole(memberId: string, roleId: string): Promise<void>;
41
+ close(): void;
42
+ reset(): void;
43
+ }
44
+ export {};
package/dist/bot.js ADDED
@@ -0,0 +1,212 @@
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
+ export default class DiscordBot {
8
+ static instance;
9
+ commands = {};
10
+ eventHandlers = [];
11
+ resolveReady;
12
+ ready = new Promise(resolve => { this.resolveReady = resolve; });
13
+ client = null;
14
+ constructor() {
15
+ if (DiscordBot.instance)
16
+ return DiscordBot.instance;
17
+ DiscordBot.instance = this;
18
+ }
19
+ async init() {
20
+ // Self-register so log.discord works even when @stonyx/discord is in the
21
+ // consumer's `dependencies` (stonyx loader only merges devDependencies).
22
+ const { logColor = '#7289da', logMethod = 'discord' } = config.discord;
23
+ log.defineType(logMethod, logColor);
24
+ if (this.client) {
25
+ // Already initialized — singleton reuse via Discord.init() or a direct
26
+ // new DiscordBot().init() call. Wait for the existing ready promise
27
+ // rather than re-running discovery + login.
28
+ await this.ready;
29
+ return;
30
+ }
31
+ const { token } = config.discord;
32
+ if (!token) {
33
+ log.discord?.('No DISCORD_TOKEN configured — bot will not start');
34
+ this.resolveReady();
35
+ return;
36
+ }
37
+ await this.discoverCommands();
38
+ await this.discoverEvents();
39
+ const intents = deriveIntents(this.eventHandlers, config.discord.additionalIntents);
40
+ const partials = derivePartials(intents, config.discord.additionalPartials);
41
+ if (Object.keys(this.commands).length > 0) {
42
+ intents.push(GatewayIntentBits.Guilds);
43
+ }
44
+ this.client = new Client({ intents: [...new Set(intents)], partials });
45
+ this.registerClientEvents();
46
+ this.client.login(token);
47
+ await this.ready;
48
+ }
49
+ registerClientEvents() {
50
+ const { client } = this;
51
+ if (!client)
52
+ return;
53
+ client.on('ready', () => {
54
+ this.resolveReady();
55
+ log.discord?.('Discord Bot is Ready!');
56
+ });
57
+ client.on('interactionCreate', async (interaction) => {
58
+ if (!interaction.isChatInputCommand())
59
+ return;
60
+ const { commandName } = interaction;
61
+ const command = this.commands[commandName];
62
+ if (!command) {
63
+ await interaction.reply({ content: `\`/${commandName}\` is not available`, flags: MessageFlags.Ephemeral });
64
+ return;
65
+ }
66
+ try {
67
+ await command.execute(interaction);
68
+ }
69
+ catch (error) {
70
+ log.error(String(error));
71
+ const reply = { content: 'There was an error executing this command!', flags: MessageFlags.Ephemeral };
72
+ if (interaction.replied || interaction.deferred) {
73
+ await interaction.followUp(reply);
74
+ }
75
+ else {
76
+ await interaction.reply(reply);
77
+ }
78
+ }
79
+ });
80
+ for (const handler of this.eventHandlers) {
81
+ const event = handler.constructor.event;
82
+ if (event) {
83
+ client.on(event, (...args) => handler.handle(...args));
84
+ }
85
+ }
86
+ }
87
+ async discoverCommands() {
88
+ const { commandDir } = config.discord;
89
+ if (!commandDir)
90
+ return;
91
+ await forEachFileImport(commandDir, (CommandClassUntyped, { name }) => {
92
+ const CommandClass = CommandClassUntyped;
93
+ const instance = new CommandClass();
94
+ if (!instance.data || typeof instance.execute !== 'function') {
95
+ log.discord?.(`Command "${name}" is missing data or execute — skipping`);
96
+ return;
97
+ }
98
+ instance._bot = this;
99
+ this.commands[instance.data.name] = instance;
100
+ log.discord?.(`Loaded command: /${instance.data.name}`);
101
+ }, { ignoreAccessFailure: true });
102
+ }
103
+ async discoverEvents() {
104
+ const { eventDir } = config.discord;
105
+ if (!eventDir)
106
+ return;
107
+ await forEachFileImport(eventDir, (EventHandlerClassUntyped, { name }) => {
108
+ const EventHandlerClass = EventHandlerClassUntyped;
109
+ if (!EventHandlerClass.event) {
110
+ log.discord?.(`Event handler "${name}" is missing static event property — skipping`);
111
+ return;
112
+ }
113
+ const instance = new EventHandlerClass();
114
+ instance._bot = this;
115
+ this.eventHandlers.push(instance);
116
+ log.discord?.(`Loaded event handler: ${EventHandlerClass.event} (${name})`);
117
+ }, { ignoreAccessFailure: true });
118
+ }
119
+ async sendMessage(content, channelId, imagePath = null) {
120
+ if (!this.client)
121
+ throw new Error('Discord bot is not initialized');
122
+ const channel = await this.client.channels.fetch(channelId);
123
+ if (!channel)
124
+ throw new Error('Invalid Channel ID');
125
+ const options = { content };
126
+ if (imagePath) {
127
+ options.files = [new AttachmentBuilder(imagePath)];
128
+ }
129
+ return await channel.send(options);
130
+ }
131
+ async sendFile(file, messageObject) {
132
+ return await messageObject.edit({
133
+ content: '',
134
+ files: [{
135
+ attachment: file,
136
+ name: file.split('/').pop() ?? file
137
+ }]
138
+ });
139
+ }
140
+ async reply(interaction, content) {
141
+ if (content.length <= 2000) {
142
+ if (interaction.deferred || interaction.replied) {
143
+ await interaction.editReply({ content });
144
+ return;
145
+ }
146
+ await interaction.reply({ content });
147
+ return;
148
+ }
149
+ const [first, ...rest] = chunkMessage('', content);
150
+ if (interaction.deferred || interaction.replied) {
151
+ await interaction.editReply({ content: first });
152
+ }
153
+ else {
154
+ await interaction.reply({ content: first });
155
+ }
156
+ for (const chunk of rest) {
157
+ await interaction.followUp({ content: chunk });
158
+ }
159
+ }
160
+ async updateStatus(name, type = 0) {
161
+ if (!this.client?.user)
162
+ throw new Error('Discord bot is not initialized');
163
+ await this.client.user.setPresence({
164
+ activities: [{ name, type }],
165
+ status: 'online'
166
+ });
167
+ }
168
+ async getChannelMessages(channelId, options = {}) {
169
+ if (!this.client)
170
+ throw new Error('Discord bot is not initialized');
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
+ async getChannelMessage(channelId, messageId) {
178
+ if (!this.client)
179
+ throw new Error('Discord bot is not initialized');
180
+ const channel = await this.client.channels.fetch(channelId);
181
+ return await channel.messages.fetch(messageId);
182
+ }
183
+ async getGuild(guildId) {
184
+ if (!this.client)
185
+ throw new Error('Discord bot is not initialized');
186
+ return await this.client.guilds.fetch(guildId || config.discord.serverId || '');
187
+ }
188
+ async clearChannelMessages(channelId) {
189
+ const promises = [];
190
+ const messages = await this.getChannelMessages(channelId);
191
+ messages.forEach(message => promises.push(message.delete()));
192
+ return Promise.all(promises);
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
+ close() {
201
+ if (this.client) {
202
+ this.client.destroy();
203
+ this.client = null;
204
+ }
205
+ DiscordBot.instance = null;
206
+ }
207
+ reset() {
208
+ this.close();
209
+ this.commands = {};
210
+ this.eventHandlers = [];
211
+ }
212
+ }
@@ -0,0 +1,6 @@
1
+ import type { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
2
+ export default class Command {
3
+ static skipAuth: boolean;
4
+ data?: SlashCommandBuilder;
5
+ execute?: (interaction: ChatInputCommandInteraction) => Promise<void>;
6
+ }
@@ -1,3 +1,3 @@
1
1
  export default class Command {
2
- static skipAuth = false;
2
+ static skipAuth = false;
3
3
  }
@@ -0,0 +1,4 @@
1
+ export default class EventHandler {
2
+ static event: string | null;
3
+ handle?: (...args: unknown[]) => void | Promise<void>;
4
+ }
@@ -1,3 +1,3 @@
1
1
  export default class EventHandler {
2
- static event = null;
2
+ static event = null;
3
3
  }
@@ -0,0 +1,11 @@
1
+ import { GatewayIntentBits, Partials } from 'discord.js';
2
+ interface EventHandlerLike {
3
+ constructor: {
4
+ event: string | null;
5
+ };
6
+ }
7
+ declare const EVENT_INTENT_MAP: Record<string, GatewayIntentBits[]>;
8
+ declare const INTENT_PARTIAL_MAP: Partial<Record<GatewayIntentBits, Partials[]>>;
9
+ export declare function deriveIntents(eventHandlers: EventHandlerLike[], additionalIntents?: string[]): GatewayIntentBits[];
10
+ export declare function derivePartials(intents: GatewayIntentBits[], additionalPartials?: string[]): Partials[];
11
+ export { EVENT_INTENT_MAP, INTENT_PARTIAL_MAP };
@@ -0,0 +1,51 @@
1
+ import { GatewayIntentBits, Partials } from 'discord.js';
2
+ const EVENT_INTENT_MAP = {
3
+ messageCreate: [GatewayIntentBits.GuildMessages, GatewayIntentBits.DirectMessages, GatewayIntentBits.MessageContent],
4
+ messageDelete: [GatewayIntentBits.GuildMessages],
5
+ messageUpdate: [GatewayIntentBits.GuildMessages],
6
+ guildMemberAdd: [GatewayIntentBits.GuildMembers],
7
+ guildMemberRemove: [GatewayIntentBits.GuildMembers],
8
+ inviteCreate: [GatewayIntentBits.GuildInvites],
9
+ inviteDelete: [GatewayIntentBits.GuildInvites],
10
+ voiceStateUpdate: [GatewayIntentBits.GuildVoiceStates],
11
+ interactionCreate: [],
12
+ };
13
+ const INTENT_PARTIAL_MAP = {
14
+ [GatewayIntentBits.DirectMessages]: [Partials.Channel],
15
+ };
16
+ export function deriveIntents(eventHandlers, additionalIntents = []) {
17
+ const intents = new Set([GatewayIntentBits.Guilds]);
18
+ for (const handler of eventHandlers) {
19
+ const event = handler.constructor.event;
20
+ if (!event)
21
+ continue;
22
+ const required = EVENT_INTENT_MAP[event];
23
+ if (required) {
24
+ for (const intent of required)
25
+ intents.add(intent);
26
+ }
27
+ }
28
+ for (const name of additionalIntents) {
29
+ const intent = GatewayIntentBits[name];
30
+ if (intent !== undefined)
31
+ intents.add(intent);
32
+ }
33
+ return [...intents];
34
+ }
35
+ export function derivePartials(intents, additionalPartials = []) {
36
+ const partials = new Set();
37
+ for (const intent of intents) {
38
+ const required = INTENT_PARTIAL_MAP[intent];
39
+ if (required) {
40
+ for (const partial of required)
41
+ partials.add(partial);
42
+ }
43
+ }
44
+ for (const name of additionalPartials) {
45
+ const partial = Partials[name];
46
+ if (partial !== undefined)
47
+ partials.add(partial);
48
+ }
49
+ return [...partials];
50
+ }
51
+ export { EVENT_INTENT_MAP, INTENT_PARTIAL_MAP };
package/dist/main.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export default class Discord {
2
+ static instance: Discord | null;
3
+ constructor();
4
+ init(): Promise<void>;
5
+ reset(): void;
6
+ }
package/dist/main.js ADDED
@@ -0,0 +1,15 @@
1
+ import DiscordBot from './bot.js';
2
+ export default class Discord {
3
+ static instance;
4
+ constructor() {
5
+ if (Discord.instance)
6
+ return Discord.instance;
7
+ Discord.instance = this;
8
+ }
9
+ async init() {
10
+ await new DiscordBot().init();
11
+ }
12
+ reset() {
13
+ Discord.instance = null;
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ export declare function chunkMessage(header: string, body: string): string[];
@@ -0,0 +1,25 @@
1
+ const MAX_LENGTH = 2000;
2
+ function splitAtBoundary(text, max) {
3
+ if (text.length <= max)
4
+ return [text, ''];
5
+ const region = text.slice(0, max);
6
+ const newlineIdx = region.lastIndexOf('\n');
7
+ if (newlineIdx > 0)
8
+ return [text.slice(0, newlineIdx + 1), text.slice(newlineIdx + 1)];
9
+ const spaceIdx = region.lastIndexOf(' ');
10
+ if (spaceIdx > 0)
11
+ return [text.slice(0, spaceIdx + 1), text.slice(spaceIdx + 1)];
12
+ return [text.slice(0, max), text.slice(max)];
13
+ }
14
+ export function chunkMessage(header, body) {
15
+ const chunks = [];
16
+ const firstChunkMax = MAX_LENGTH - header.length;
17
+ let [first, remaining] = splitAtBoundary(body, firstChunkMax);
18
+ chunks.push(header + first);
19
+ while (remaining.length > 0) {
20
+ let chunk;
21
+ [chunk, remaining] = splitAtBoundary(remaining, MAX_LENGTH);
22
+ chunks.push(chunk);
23
+ }
24
+ return chunks;
25
+ }
package/package.json CHANGED
@@ -4,22 +4,42 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.1.1-beta.5",
7
+ "version": "0.1.1-beta.50",
8
8
  "description": "Discord bot module for the Stonyx framework",
9
- "main": "src/main.js",
9
+ "main": "dist/main.js",
10
+ "types": "dist/main.d.ts",
10
11
  "type": "module",
11
12
  "files": [
12
- "src",
13
+ "dist",
13
14
  "config",
14
15
  "LICENSE.md",
15
16
  "README.md"
16
17
  ],
17
18
  "exports": {
18
- ".": "./src/main.js",
19
- "./bot": "./src/bot.js",
20
- "./command": "./src/command.js",
21
- "./event-handler": "./src/event-handler.js",
22
- "./message": "./src/message.js"
19
+ ".": {
20
+ "types": "./dist/main.d.ts",
21
+ "default": "./dist/main.js"
22
+ },
23
+ "./bot": {
24
+ "types": "./dist/bot.d.ts",
25
+ "default": "./dist/bot.js"
26
+ },
27
+ "./command": {
28
+ "types": "./dist/command.d.ts",
29
+ "default": "./dist/command.js"
30
+ },
31
+ "./event-handler": {
32
+ "types": "./dist/event-handler.d.ts",
33
+ "default": "./dist/event-handler.js"
34
+ },
35
+ "./message": {
36
+ "types": "./dist/message.d.ts",
37
+ "default": "./dist/message.js"
38
+ },
39
+ "./base": {
40
+ "types": "./dist/base.d.ts",
41
+ "default": "./dist/base.js"
42
+ }
23
43
  },
24
44
  "publishConfig": {
25
45
  "access": "public"
@@ -44,12 +64,19 @@
44
64
  "stonyx": ">=0.2.3-beta.4"
45
65
  },
46
66
  "devDependencies": {
47
- "@stonyx/utils": "0.2.3-beta.7",
67
+ "@stonyx/utils": "0.2.3-beta.23",
68
+ "@types/node": "^25.5.2",
69
+ "@types/qunit": "^2.19.13",
70
+ "@types/sinon": "^21.0.1",
48
71
  "qunit": "^2.24.1",
49
72
  "sinon": "^21.0.0",
50
- "stonyx": "0.2.3-beta.12"
73
+ "stonyx": "0.2.3-beta.63",
74
+ "tsx": "^4.21.0",
75
+ "typescript": "^5.8.3"
51
76
  },
52
77
  "scripts": {
53
- "test": "stonyx test"
78
+ "build": "tsc",
79
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
80
+ "test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
54
81
  }
55
82
  }
package/src/bot.js DELETED
@@ -1,214 +0,0 @@
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/intents.js DELETED
@@ -1,56 +0,0 @@
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 };
package/src/main.js DELETED
@@ -1,15 +0,0 @@
1
- export default class Discord {
2
- constructor() {
3
- if (Discord.instance) return Discord.instance;
4
- Discord.instance = this;
5
- }
6
-
7
- async init() {
8
- // Bot initialization is deferred to DiscordBot.init()
9
- // This entry point satisfies Stonyx module auto-initialization
10
- }
11
-
12
- reset() {
13
- Discord.instance = null;
14
- }
15
- }
package/src/message.js DELETED
@@ -1,30 +0,0 @@
1
- const MAX_LENGTH = 2000;
2
-
3
- function splitAtBoundary(text, max) {
4
- if (text.length <= max) return [text, ''];
5
-
6
- const region = text.slice(0, max);
7
- const newlineIdx = region.lastIndexOf('\n');
8
- if (newlineIdx > 0) return [text.slice(0, newlineIdx + 1), text.slice(newlineIdx + 1)];
9
-
10
- const spaceIdx = region.lastIndexOf(' ');
11
- if (spaceIdx > 0) return [text.slice(0, spaceIdx + 1), text.slice(spaceIdx + 1)];
12
-
13
- return [text.slice(0, max), text.slice(max)];
14
- }
15
-
16
- export function chunkMessage(header, body) {
17
- const chunks = [];
18
- const firstChunkMax = MAX_LENGTH - header.length;
19
-
20
- let [first, remaining] = splitAtBoundary(body, firstChunkMax);
21
- chunks.push(header + first);
22
-
23
- while (remaining.length > 0) {
24
- let chunk;
25
- [chunk, remaining] = splitAtBoundary(remaining, MAX_LENGTH);
26
- chunks.push(chunk);
27
- }
28
-
29
- return chunks;
30
- }