@stonyx/discord 0.1.1-beta.12 → 0.1.1-beta.13
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/dist/base.js +1 -0
- package/dist/bot.d.ts +44 -0
- package/dist/bot.js +200 -0
- package/dist/command.d.ts +6 -0
- package/{src → dist}/command.js +1 -1
- package/dist/event-handler.d.ts +4 -0
- package/{src → dist}/event-handler.js +1 -1
- package/dist/intents.d.ts +11 -0
- package/dist/intents.js +51 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +15 -0
- package/dist/message.d.ts +1 -0
- package/dist/message.js +25 -0
- package/package.json +34 -11
- package/src/bot.js +0 -214
- package/src/intents.js +0 -56
- package/src/main.js +0 -15
- package/src/message.js +0 -30
- /package/{src/base.js → dist/base.d.ts} +0 -0
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,200 @@
|
|
|
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
|
+
const { token } = config.discord;
|
|
21
|
+
if (!token) {
|
|
22
|
+
log.discord('No DISCORD_TOKEN configured — bot will not start');
|
|
23
|
+
this.resolveReady();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await this.discoverCommands();
|
|
27
|
+
await this.discoverEvents();
|
|
28
|
+
const hasWork = Object.keys(this.commands).length > 0 || this.eventHandlers.length > 0;
|
|
29
|
+
if (!hasWork) {
|
|
30
|
+
log.discord('No discord commands or event handlers found — skipping bot initialization');
|
|
31
|
+
this.resolveReady();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const discordConfig = config.discord;
|
|
35
|
+
const intents = deriveIntents(this.eventHandlers, discordConfig.additionalIntents);
|
|
36
|
+
const partials = derivePartials(intents, discordConfig.additionalPartials);
|
|
37
|
+
if (Object.keys(this.commands).length > 0) {
|
|
38
|
+
intents.push(GatewayIntentBits.Guilds);
|
|
39
|
+
}
|
|
40
|
+
this.client = new Client({ intents: [...new Set(intents)], partials });
|
|
41
|
+
this.registerClientEvents();
|
|
42
|
+
this.client.login(token);
|
|
43
|
+
await this.ready;
|
|
44
|
+
}
|
|
45
|
+
registerClientEvents() {
|
|
46
|
+
const { client } = this;
|
|
47
|
+
if (!client)
|
|
48
|
+
return;
|
|
49
|
+
client.on('ready', () => {
|
|
50
|
+
this.resolveReady();
|
|
51
|
+
log.discord('Discord Bot is Ready!');
|
|
52
|
+
});
|
|
53
|
+
client.on('interactionCreate', async (interaction) => {
|
|
54
|
+
if (!interaction.isChatInputCommand())
|
|
55
|
+
return;
|
|
56
|
+
const { commandName } = interaction;
|
|
57
|
+
const command = this.commands[commandName];
|
|
58
|
+
if (!command) {
|
|
59
|
+
await interaction.reply({ content: `\`/${commandName}\` is not available`, flags: MessageFlags.Ephemeral });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
await command.execute(interaction);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
log.error(String(error));
|
|
67
|
+
const reply = { content: 'There was an error executing this command!', flags: MessageFlags.Ephemeral };
|
|
68
|
+
if (interaction.replied || interaction.deferred) {
|
|
69
|
+
await interaction.followUp(reply);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await interaction.reply(reply);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
for (const handler of this.eventHandlers) {
|
|
77
|
+
const event = handler.constructor.event;
|
|
78
|
+
if (event) {
|
|
79
|
+
client.on(event, (...args) => handler.handle(...args));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async discoverCommands() {
|
|
84
|
+
const { commandDir } = config.discord;
|
|
85
|
+
if (!commandDir)
|
|
86
|
+
return;
|
|
87
|
+
await forEachFileImport(commandDir, (CommandClassUntyped, { name }) => {
|
|
88
|
+
const CommandClass = CommandClassUntyped;
|
|
89
|
+
const instance = new CommandClass();
|
|
90
|
+
if (!instance.data || typeof instance.execute !== 'function') {
|
|
91
|
+
log.discord(`Command "${name}" is missing data or execute — skipping`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
instance._bot = this;
|
|
95
|
+
this.commands[instance.data.name] = instance;
|
|
96
|
+
log.discord(`Loaded command: /${instance.data.name}`);
|
|
97
|
+
}, { ignoreAccessFailure: true });
|
|
98
|
+
}
|
|
99
|
+
async discoverEvents() {
|
|
100
|
+
const { eventDir } = config.discord;
|
|
101
|
+
if (!eventDir)
|
|
102
|
+
return;
|
|
103
|
+
await forEachFileImport(eventDir, (EventHandlerClassUntyped, { name }) => {
|
|
104
|
+
const EventHandlerClass = EventHandlerClassUntyped;
|
|
105
|
+
if (!EventHandlerClass.event) {
|
|
106
|
+
log.discord(`Event handler "${name}" is missing static event property — skipping`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const instance = new EventHandlerClass();
|
|
110
|
+
instance._bot = this;
|
|
111
|
+
this.eventHandlers.push(instance);
|
|
112
|
+
log.discord(`Loaded event handler: ${EventHandlerClass.event} (${name})`);
|
|
113
|
+
}, { ignoreAccessFailure: true });
|
|
114
|
+
}
|
|
115
|
+
async sendMessage(content, channelId, imagePath = null) {
|
|
116
|
+
const channel = await this.client.channels.fetch(channelId);
|
|
117
|
+
if (!channel)
|
|
118
|
+
throw new Error('Invalid Channel ID');
|
|
119
|
+
const options = { content };
|
|
120
|
+
if (imagePath) {
|
|
121
|
+
options.files = [new AttachmentBuilder(imagePath)];
|
|
122
|
+
}
|
|
123
|
+
return await channel.send(options);
|
|
124
|
+
}
|
|
125
|
+
async sendFile(file, messageObject) {
|
|
126
|
+
return await messageObject.edit({
|
|
127
|
+
content: '',
|
|
128
|
+
files: [{
|
|
129
|
+
attachment: file,
|
|
130
|
+
name: file.split('/').pop()
|
|
131
|
+
}]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async reply(interaction, content) {
|
|
135
|
+
if (content.length <= 2000) {
|
|
136
|
+
if (interaction.deferred || interaction.replied) {
|
|
137
|
+
await interaction.editReply({ content });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await interaction.reply({ content });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const [first, ...rest] = chunkMessage('', content);
|
|
144
|
+
if (interaction.deferred || interaction.replied) {
|
|
145
|
+
await interaction.editReply({ content: first });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await interaction.reply({ content: first });
|
|
149
|
+
}
|
|
150
|
+
for (const chunk of rest) {
|
|
151
|
+
await interaction.followUp({ content: chunk });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async updateStatus(name, type = 0) {
|
|
155
|
+
await this.client.user.setPresence({
|
|
156
|
+
activities: [{ name, type }],
|
|
157
|
+
status: 'online'
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async getChannelMessages(channelId, options = {}) {
|
|
161
|
+
const channel = await this.client.channels.fetch(channelId);
|
|
162
|
+
const discordConfig = config.discord;
|
|
163
|
+
return await channel.messages.fetch({
|
|
164
|
+
limit: discordConfig.maxMessagesPerRequest,
|
|
165
|
+
...options
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async getChannelMessage(channelId, messageId) {
|
|
169
|
+
const channel = await this.client.channels.fetch(channelId);
|
|
170
|
+
return await channel.messages.fetch(messageId);
|
|
171
|
+
}
|
|
172
|
+
async getGuild(guildId) {
|
|
173
|
+
const discordConfig = config.discord;
|
|
174
|
+
return await this.client.guilds.fetch(guildId || discordConfig.serverId);
|
|
175
|
+
}
|
|
176
|
+
async clearChannelMessages(channelId) {
|
|
177
|
+
const promises = [];
|
|
178
|
+
const messages = await this.getChannelMessages(channelId);
|
|
179
|
+
messages.forEach(message => promises.push(message.delete()));
|
|
180
|
+
return Promise.all(promises);
|
|
181
|
+
}
|
|
182
|
+
async giveRole(memberId, roleId) {
|
|
183
|
+
const guild = await this.getGuild();
|
|
184
|
+
const role = await guild.roles.fetch(roleId);
|
|
185
|
+
const member = await guild.members.fetch(memberId);
|
|
186
|
+
await member.roles.add(role);
|
|
187
|
+
}
|
|
188
|
+
close() {
|
|
189
|
+
if (this.client) {
|
|
190
|
+
this.client.destroy();
|
|
191
|
+
this.client = null;
|
|
192
|
+
}
|
|
193
|
+
DiscordBot.instance = null;
|
|
194
|
+
}
|
|
195
|
+
reset() {
|
|
196
|
+
this.close();
|
|
197
|
+
this.commands = {};
|
|
198
|
+
this.eventHandlers = [];
|
|
199
|
+
}
|
|
200
|
+
}
|
package/{src → dist}/command.js
RENAMED
|
@@ -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 };
|
package/dist/intents.js
ADDED
|
@@ -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
package/dist/main.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default class Discord {
|
|
2
|
+
static instance;
|
|
3
|
+
constructor() {
|
|
4
|
+
if (Discord.instance)
|
|
5
|
+
return Discord.instance;
|
|
6
|
+
Discord.instance = this;
|
|
7
|
+
}
|
|
8
|
+
async init() {
|
|
9
|
+
// Bot initialization is deferred to DiscordBot.init()
|
|
10
|
+
// This entry point satisfies Stonyx module auto-initialization
|
|
11
|
+
}
|
|
12
|
+
reset() {
|
|
13
|
+
Discord.instance = null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function chunkMessage(header: string, body: string): string[];
|
package/dist/message.js
ADDED
|
@@ -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,23 +4,42 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.1.1-beta.
|
|
7
|
+
"version": "0.1.1-beta.13",
|
|
8
8
|
"description": "Discord bot module for the Stonyx framework",
|
|
9
|
-
"main": "
|
|
9
|
+
"main": "dist/main.js",
|
|
10
|
+
"types": "dist/main.d.ts",
|
|
10
11
|
"type": "module",
|
|
11
12
|
"files": [
|
|
12
|
-
"
|
|
13
|
+
"dist",
|
|
13
14
|
"config",
|
|
14
15
|
"LICENSE.md",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"exports": {
|
|
18
|
-
".":
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"./
|
|
23
|
-
|
|
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
|
+
}
|
|
24
43
|
},
|
|
25
44
|
"publishConfig": {
|
|
26
45
|
"access": "public"
|
|
@@ -46,11 +65,15 @@
|
|
|
46
65
|
},
|
|
47
66
|
"devDependencies": {
|
|
48
67
|
"@stonyx/utils": "0.2.3-beta.7",
|
|
68
|
+
"@types/node": "^25.5.2",
|
|
49
69
|
"qunit": "^2.24.1",
|
|
50
70
|
"sinon": "^21.0.0",
|
|
51
|
-
"stonyx": "0.2.3-beta.12"
|
|
71
|
+
"stonyx": "0.2.3-beta.12",
|
|
72
|
+
"typescript": "^5.8.3"
|
|
52
73
|
},
|
|
53
74
|
"scripts": {
|
|
54
|
-
"
|
|
75
|
+
"build": "tsc",
|
|
76
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
77
|
+
"test": "pnpm build && pnpm build:test && stonyx test 'dist-test/test/**/*-test.js'"
|
|
55
78
|
}
|
|
56
79
|
}
|
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
|
-
}
|
|
File without changes
|