spiralcord-full 2.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/bin/spiral.js +496 -0
- package/package.json +55 -0
- package/src/index.js +7 -0
- package/src/plugins/manager.js +413 -0
- package/src/runtime.js +180 -0
- package/src/test.js +361 -0
- package/src/web/public/index.html +358 -0
- package/src/web/server.js +184 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const fsSync = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
|
|
6
|
+
class SpiralPluginManager extends EventEmitter {
|
|
7
|
+
constructor(runtime) {
|
|
8
|
+
super();
|
|
9
|
+
this.runtime = runtime;
|
|
10
|
+
this.plugins = new Map();
|
|
11
|
+
this.commands = new Map();
|
|
12
|
+
this.pluginAPIs = new Map();
|
|
13
|
+
this.pluginConfigs = new Map();
|
|
14
|
+
this.loadOrder = [];
|
|
15
|
+
this.maxListeners = 50;
|
|
16
|
+
this._cache = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async loadPlugins(pluginsDir) {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(pluginsDir);
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const pluginFolders = await fs.readdir(pluginsDir);
|
|
27
|
+
const manifests = [];
|
|
28
|
+
|
|
29
|
+
const manifestPromises = pluginFolders.map(async (folder) => {
|
|
30
|
+
const pluginPath = path.resolve(pluginsDir, folder);
|
|
31
|
+
const manifestPath = path.join(pluginPath, 'plugin.json');
|
|
32
|
+
try {
|
|
33
|
+
await fs.access(manifestPath);
|
|
34
|
+
const data = await fs.readFile(manifestPath, 'utf-8');
|
|
35
|
+
return { folder, pluginPath, manifest: JSON.parse(data) };
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const results = await Promise.all(manifestPromises);
|
|
42
|
+
for (const r of results) {
|
|
43
|
+
if (r) manifests.push(r);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sorted = this.resolveDependencies(manifests);
|
|
47
|
+
|
|
48
|
+
const loadPromises = sorted.map(({ folder, pluginPath, manifest }) =>
|
|
49
|
+
this.loadPlugin(folder, pluginPath, manifest)
|
|
50
|
+
);
|
|
51
|
+
await Promise.all(loadPromises);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
resolveDependencies(manifests) {
|
|
55
|
+
const manifestMap = new Map();
|
|
56
|
+
for (const m of manifests) {
|
|
57
|
+
manifestMap.set(m.manifest.name, m);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const resolved = [];
|
|
61
|
+
const visited = new Set();
|
|
62
|
+
const visiting = new Set();
|
|
63
|
+
|
|
64
|
+
const visit = (name) => {
|
|
65
|
+
if (visited.has(name)) return;
|
|
66
|
+
if (visiting.has(name)) {
|
|
67
|
+
console.error(`[manager] Circular dependency: ${name}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
visiting.add(name);
|
|
71
|
+
|
|
72
|
+
const m = manifestMap.get(name);
|
|
73
|
+
if (m && m.manifest.dependencies) {
|
|
74
|
+
for (const dep of Object.keys(m.manifest.dependencies)) {
|
|
75
|
+
visit(dep);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
visiting.delete(name);
|
|
80
|
+
visited.add(name);
|
|
81
|
+
if (m) resolved.push(m);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const m of manifests) {
|
|
85
|
+
visit(m.manifest.name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async loadPlugin(folder, pluginPath, manifest) {
|
|
92
|
+
const indexPath = path.join(pluginPath, 'index.js');
|
|
93
|
+
const dslPath = path.join(pluginPath, 'plugin.dsl');
|
|
94
|
+
const configSchemaPath = path.join(pluginPath, 'config.schema.json');
|
|
95
|
+
const configPath = path.join(pluginPath, 'config.json');
|
|
96
|
+
|
|
97
|
+
const [config, pluginModule] = await Promise.all([
|
|
98
|
+
this.loadPluginConfig(configSchemaPath, configPath),
|
|
99
|
+
this._loadModule(indexPath, dslPath, manifest)
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
if (!pluginModule) return;
|
|
103
|
+
|
|
104
|
+
this.pluginConfigs.set(manifest.name, config);
|
|
105
|
+
this.plugins.set(manifest.name, { manifest, module: pluginModule, path: pluginPath });
|
|
106
|
+
|
|
107
|
+
if (pluginModule.init) {
|
|
108
|
+
await pluginModule.init(config, this.runtime);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (pluginModule.commands) {
|
|
112
|
+
for (const [name, cmd] of Object.entries(pluginModule.commands)) {
|
|
113
|
+
this.registerCommand(manifest.name, name, cmd);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (pluginModule.keywords) {
|
|
118
|
+
for (const [kw, handler] of Object.entries(pluginModule.keywords)) {
|
|
119
|
+
this.registerCommand(manifest.name, kw, { execute: handler, isKeyword: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (pluginModule.hooks) {
|
|
124
|
+
for (const [hookName, handler] of Object.entries(pluginModule.hooks)) {
|
|
125
|
+
this.on(hookName, async (payload) => {
|
|
126
|
+
try {
|
|
127
|
+
await handler(payload, this.runtime);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(`[manager] Hook error ${hookName} from ${manifest.name}:`, error.message);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (pluginModule.api) {
|
|
136
|
+
this.pluginAPIs.set(manifest.name, pluginModule.api);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.loadOrder.push(manifest.name);
|
|
140
|
+
console.log(`[manager] Loaded ${manifest.name} v${manifest.version}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async _loadModule(indexPath, dslPath, manifest) {
|
|
144
|
+
try {
|
|
145
|
+
await fs.access(indexPath);
|
|
146
|
+
delete require.cache[require.resolve(indexPath)];
|
|
147
|
+
return require(indexPath);
|
|
148
|
+
} catch {}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await fs.access(dslPath);
|
|
152
|
+
const dslContent = await fs.readFile(dslPath, 'utf-8');
|
|
153
|
+
return this.compileDSL(dslContent, manifest);
|
|
154
|
+
} catch {}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
registerCommand(pluginName, name, cmd) {
|
|
160
|
+
if (!this.commands.has(name)) {
|
|
161
|
+
this.commands.set(name, []);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const registrations = this.commands.get(name);
|
|
165
|
+
const existing = registrations.find(r => r.plugin === pluginName);
|
|
166
|
+
|
|
167
|
+
if (existing) {
|
|
168
|
+
existing.handler = cmd.execute || cmd;
|
|
169
|
+
existing.description = cmd.description || existing.description;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const manifest = this.plugins.get(pluginName)?.manifest;
|
|
174
|
+
const collisionPolicy = manifest?.collision_policy || 'last-wins';
|
|
175
|
+
|
|
176
|
+
if (registrations.length > 0) {
|
|
177
|
+
const conflict = registrations[0];
|
|
178
|
+
console.warn(`[manager] Command "${name}" collision: ${conflict.plugin} vs ${pluginName}`);
|
|
179
|
+
|
|
180
|
+
if (collisionPolicy === 'error') {
|
|
181
|
+
console.error(`[manager] Refusing ${pluginName}: "${name}" already registered`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
registrations.push({
|
|
187
|
+
plugin: pluginName,
|
|
188
|
+
handler: cmd.execute || cmd,
|
|
189
|
+
description: cmd.description || '',
|
|
190
|
+
usage: cmd.usage || '',
|
|
191
|
+
slash: cmd.slash || false,
|
|
192
|
+
priority: cmd.priority || 0
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
registrations.sort((a, b) => b.priority - a.priority);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getCommand(name) {
|
|
199
|
+
const registrations = this.commands.get(name);
|
|
200
|
+
if (!registrations || registrations.length === 0) return null;
|
|
201
|
+
return registrations[0];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getAllCommands() {
|
|
205
|
+
const result = new Map();
|
|
206
|
+
for (const [name, registrations] of this.commands) {
|
|
207
|
+
if (registrations.length > 0) {
|
|
208
|
+
result.set(name, registrations[0]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getSlashCommands() {
|
|
215
|
+
const result = [];
|
|
216
|
+
for (const [name, registrations] of this.commands) {
|
|
217
|
+
const cmd = registrations[0];
|
|
218
|
+
if (cmd && cmd.slash) {
|
|
219
|
+
result.push({ name, description: cmd.description });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async executeCommand(name, message, args) {
|
|
226
|
+
const cmd = this.getCommand(name);
|
|
227
|
+
if (!cmd) return false;
|
|
228
|
+
|
|
229
|
+
const config = this.runtime.config;
|
|
230
|
+
const disabledCommands = config.disabledCommands || [];
|
|
231
|
+
if (disabledCommands.includes(name)) {
|
|
232
|
+
await message.reply(`Command \`${name}\` has been disabled.`);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
await cmd.handler(message, args, this.runtime);
|
|
238
|
+
return true;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(`[manager] Error executing ${name}:`, error);
|
|
241
|
+
await message.reply('An error occurred while executing this command.');
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async emitHook(eventName, payload) {
|
|
247
|
+
this.emit(eventName, payload);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async emitHookSerial(eventName, payload) {
|
|
251
|
+
const listeners = this.listeners(eventName);
|
|
252
|
+
for (const listener of listeners) {
|
|
253
|
+
await listener(payload);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async emitHookWithResult(eventName, payload) {
|
|
258
|
+
const listeners = this.listeners(eventName);
|
|
259
|
+
for (const listener of listeners) {
|
|
260
|
+
const result = await listener(payload);
|
|
261
|
+
if (result === true) return true;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async loadPluginConfig(schemaPath, configPath) {
|
|
267
|
+
let config = {};
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const schemaData = await fs.readFile(schemaPath, 'utf-8');
|
|
271
|
+
config = this.extractDefaults(JSON.parse(schemaData));
|
|
272
|
+
} catch {}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const configData = await fs.readFile(configPath, 'utf-8');
|
|
276
|
+
config = { ...config, ...JSON.parse(configData) };
|
|
277
|
+
} catch {}
|
|
278
|
+
|
|
279
|
+
return config;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
extractDefaults(schema) {
|
|
283
|
+
const defaults = {};
|
|
284
|
+
if (!schema.properties) return defaults;
|
|
285
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
286
|
+
if (prop.default !== undefined) defaults[key] = prop.default;
|
|
287
|
+
}
|
|
288
|
+
return defaults;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
getPluginConfig(pluginName) {
|
|
292
|
+
return this.pluginConfigs.get(pluginName) || {};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getPluginAPI(pluginName) {
|
|
296
|
+
return this.pluginAPIs.get(pluginName) || null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
getPlugins() {
|
|
300
|
+
return Array.from(this.plugins.keys());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
getPluginInfo(pluginName) {
|
|
304
|
+
const plugin = this.plugins.get(pluginName);
|
|
305
|
+
if (!plugin) return null;
|
|
306
|
+
return {
|
|
307
|
+
manifest: plugin.manifest,
|
|
308
|
+
path: plugin.path,
|
|
309
|
+
config: this.pluginConfigs.get(pluginName)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
compileDSL(dslContent, manifest) {
|
|
314
|
+
const lines = dslContent.split('\n');
|
|
315
|
+
const commands = {};
|
|
316
|
+
|
|
317
|
+
let currentBlock = null;
|
|
318
|
+
let blockLines = [];
|
|
319
|
+
|
|
320
|
+
for (const line of lines) {
|
|
321
|
+
const trimmed = line.trim();
|
|
322
|
+
if (trimmed === '' || trimmed.startsWith('#')) continue;
|
|
323
|
+
|
|
324
|
+
if (trimmed.startsWith('COMMAND ')) {
|
|
325
|
+
if (currentBlock) {
|
|
326
|
+
this._processDSLBlock(currentBlock, blockLines, commands);
|
|
327
|
+
blockLines = [];
|
|
328
|
+
}
|
|
329
|
+
const match = trimmed.match(/COMMAND\s+(\S+)\s+"([^"]+)"/);
|
|
330
|
+
if (match) {
|
|
331
|
+
currentBlock = { type: 'command', name: match[1], description: match[2], aliases: [], cooldown: 0, requiresArgs: null, requiresPermission: null, response: null, responsePool: null, embed: null };
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (trimmed === 'END') {
|
|
337
|
+
if (currentBlock) {
|
|
338
|
+
this._processDSLBlock(currentBlock, blockLines, commands);
|
|
339
|
+
currentBlock = null;
|
|
340
|
+
blockLines = [];
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (currentBlock) blockLines.push(trimmed);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { commands, init: async (config, runtime) => {} };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_processDSLBlock(block, lines, commands) {
|
|
352
|
+
if (block.type !== 'command') return;
|
|
353
|
+
|
|
354
|
+
for (const line of lines) {
|
|
355
|
+
if (line.startsWith('ALIASES ')) block.aliases = line.slice(8).split(',').map(a => a.trim());
|
|
356
|
+
else if (line.startsWith('COOLDOWN ')) block.cooldown = parseInt(line.slice(9)) * 1000;
|
|
357
|
+
else if (line.startsWith('REQUIRES_ARGS ')) block.requiresArgs = line.slice(14).replace(/^["']|["']$/g, '');
|
|
358
|
+
else if (line.startsWith('REQUIRES_PERMISSION ')) block.requiresPermission = line.slice(20);
|
|
359
|
+
else if (line.startsWith('RESPONSE ')) block.response = line.slice(9).replace(/^["']|["']$/g, '');
|
|
360
|
+
else if (line.startsWith('RESPONSE_POOL')) block.responsePool = [];
|
|
361
|
+
else if (line.startsWith('EMBED ')) {
|
|
362
|
+
const match = line.match(/EMBED\s+"([^"]+)"/);
|
|
363
|
+
block.embed = { color: match?.[1] || '#00FF00' };
|
|
364
|
+
}
|
|
365
|
+
else if (line.startsWith('TITLE ')) { if (block.embed) block.embed.title = line.slice(6).replace(/^["']|["']$/g, ''); }
|
|
366
|
+
else if (line.startsWith('DESCRIPTION ')) { if (block.embed) block.embed.description = line.slice(12).replace(/^["']|["']$/g, ''); }
|
|
367
|
+
else if (block.responsePool !== null && !line.startsWith('TITLE') && !line.startsWith('DESCRIPTION')) {
|
|
368
|
+
block.responsePool.push(line.replace(/^["']|["']$/g, ''));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const blockRef = block;
|
|
373
|
+
const manager = this;
|
|
374
|
+
const handler = async (message, args, runtime) => {
|
|
375
|
+
if (blockRef.requiresArgs && (!args || args.length === 0)) return message.reply(blockRef.requiresArgs);
|
|
376
|
+
if (blockRef.requiresPermission && !message.member?.permissions.has(blockRef.requiresPermission)) {
|
|
377
|
+
return message.reply(`You need \`${blockRef.requiresPermission}\` permission.`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const ctxMap = new Map([
|
|
381
|
+
['user.id', message.author.id], ['user.name', message.author.username],
|
|
382
|
+
['channel.id', message.channel.id], ['guild.id', message.guild?.id || 'dm'],
|
|
383
|
+
['guild.name', message.guild?.name || 'DM'], ['args', args.join(' ')]
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
if (blockRef.response) return message.reply(manager._resolveDSLVars(blockRef.response, ctxMap));
|
|
387
|
+
if (blockRef.responsePool && blockRef.responsePool.length > 0) {
|
|
388
|
+
const response = blockRef.responsePool[Math.floor(Math.random() * blockRef.responsePool.length)];
|
|
389
|
+
return message.reply(manager._resolveDSLVars(response, ctxMap));
|
|
390
|
+
}
|
|
391
|
+
if (blockRef.embed) {
|
|
392
|
+
const { EmbedBuilder } = require('discord.js');
|
|
393
|
+
const embed = new EmbedBuilder()
|
|
394
|
+
.setColor(manager._resolveDSLVars(blockRef.embed.color, ctxMap))
|
|
395
|
+
.setTitle(manager._resolveDSLVars(blockRef.embed.title || '', ctxMap))
|
|
396
|
+
.setDescription(manager._resolveDSLVars(blockRef.embed.description || '', ctxMap));
|
|
397
|
+
return message.reply({ embeds: [embed] });
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
commands[blockRef.name] = { description: blockRef.description, execute: handler, slash: false };
|
|
402
|
+
for (const alias of blockRef.aliases) {
|
|
403
|
+
commands[alias] = { description: blockRef.description, execute: handler, slash: false };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_resolveDSLVars(text, ctxMap) {
|
|
408
|
+
if (!text) return '';
|
|
409
|
+
return text.replace(/\{(\w+)\}/g, (match, key) => ctxMap.has(key) ? ctxMap.get(key) : match);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
module.exports = SpiralPluginManager;
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const { Client, GatewayIntentBits, Events } = require('discord.js');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const SpiralPluginManager = require('./plugins/manager');
|
|
5
|
+
|
|
6
|
+
class SpiralRuntime {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config || {};
|
|
9
|
+
this.client = null;
|
|
10
|
+
this.pluginManager = new SpiralPluginManager(this);
|
|
11
|
+
this.startTime = Date.now();
|
|
12
|
+
this._pluginsDir = path.join(process.cwd(), 'plugins');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async start() {
|
|
16
|
+
const t0 = Date.now();
|
|
17
|
+
console.log('[runtime] Starting Spiralcord v' + require('../package.json').version);
|
|
18
|
+
|
|
19
|
+
this.client = new Client({ intents: this._resolveIntents() });
|
|
20
|
+
this._bindEvents();
|
|
21
|
+
|
|
22
|
+
console.log('[runtime] Loading plugins...');
|
|
23
|
+
await this.pluginManager.loadPlugins(this._pluginsDir);
|
|
24
|
+
|
|
25
|
+
const pluginCount = this.pluginManager.getPlugins().length;
|
|
26
|
+
const commandCount = this.pluginManager.getAllCommands().size;
|
|
27
|
+
console.log(`[runtime] ${pluginCount} plugins, ${commandCount} commands loaded in ${Date.now() - t0}ms`);
|
|
28
|
+
|
|
29
|
+
await this.client.login(this.config.token);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_resolveIntents() {
|
|
33
|
+
const intents = this.config.intents || ['Guilds', 'GuildMessages', 'MessageContent'];
|
|
34
|
+
return intents.map(i => {
|
|
35
|
+
const map = {
|
|
36
|
+
'Guilds': GatewayIntentBits.Guilds,
|
|
37
|
+
'GuildMessages': GatewayIntentBits.GuildMessages,
|
|
38
|
+
'MessageContent': GatewayIntentBits.MessageContent,
|
|
39
|
+
'GuildMembers': GatewayIntentBits.GuildMembers,
|
|
40
|
+
'GuildVoiceStates': GatewayIntentBits.GuildVoiceStates,
|
|
41
|
+
'DirectMessages': GatewayIntentBits.DirectMessages,
|
|
42
|
+
'GuildPresences': GatewayIntentBits.GuildPresences
|
|
43
|
+
};
|
|
44
|
+
return map[i] || GatewayIntentBits.Guilds;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_bindEvents() {
|
|
49
|
+
this.client.removeAllListeners();
|
|
50
|
+
|
|
51
|
+
this.client.on(Events.ClientReady, async () => {
|
|
52
|
+
console.log(`[runtime] Ready as ${this.client.user.tag} | ${this.client.guilds.cache.size} servers | ${this.client.users.cache.size} users`);
|
|
53
|
+
|
|
54
|
+
await this.pluginManager.emitHook('bot_ready', {
|
|
55
|
+
client: this.client,
|
|
56
|
+
user: this.client.user,
|
|
57
|
+
config: this.config
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.client.on(Events.MessageCreate, async (message) => {
|
|
62
|
+
if (message.author.bot) return;
|
|
63
|
+
await this.pluginManager.emitHook('message_received', {
|
|
64
|
+
message,
|
|
65
|
+
user: message.author,
|
|
66
|
+
guild: message.guild,
|
|
67
|
+
channel: message.channel
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.client.on(Events.InteractionCreate, async (interaction) => {
|
|
72
|
+
await this.pluginManager.emitHook('interaction_received', {
|
|
73
|
+
interaction,
|
|
74
|
+
user: interaction.user,
|
|
75
|
+
guild: interaction.guild,
|
|
76
|
+
channel: interaction.channel
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.client.on(Events.PresenceUpdate, async (oldPresence, newPresence) => {
|
|
81
|
+
await this.pluginManager.emitHook('presence_update', {
|
|
82
|
+
oldPresence,
|
|
83
|
+
newPresence,
|
|
84
|
+
user: newPresence.user,
|
|
85
|
+
guild: newPresence.guild
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.client.on(Events.GuildCreate, async (guild) => {
|
|
90
|
+
console.log(`[runtime] Joined: ${guild.name}`);
|
|
91
|
+
await this.pluginManager.emitHook('guild_joined', { guild });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.client.on(Events.GuildDelete, async (guild) => {
|
|
95
|
+
console.log(`[runtime] Left: ${guild.name}`);
|
|
96
|
+
await this.pluginManager.emitHook('guild_left', { guild });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.client.on(Events.VoiceStateUpdate, async (oldState, newState) => {
|
|
100
|
+
await this.pluginManager.emitHook('voice_state_update', {
|
|
101
|
+
oldState,
|
|
102
|
+
newState,
|
|
103
|
+
user: newState.member?.user,
|
|
104
|
+
guild: newState.guild
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.client.on(Events.MessageReactionAdd, async (reaction, user) => {
|
|
109
|
+
await this.pluginManager.emitHook('reaction_add', {
|
|
110
|
+
reaction,
|
|
111
|
+
user,
|
|
112
|
+
message: reaction.message
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.client.on(Events.MessageReactionRemove, async (reaction, user) => {
|
|
117
|
+
await this.pluginManager.emitHook('reaction_remove', {
|
|
118
|
+
reaction,
|
|
119
|
+
user,
|
|
120
|
+
message: reaction.message
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.client.on(Events.Error, (error) => {
|
|
125
|
+
console.error('[runtime] Error:', error.message);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
process.on('unhandledRejection', (error) => {
|
|
129
|
+
console.error('[runtime] Unhandled:', error);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async restart() {
|
|
134
|
+
const t0 = Date.now();
|
|
135
|
+
console.log('[runtime] Restarting...');
|
|
136
|
+
|
|
137
|
+
await this.pluginManager.emitHook('bot_shutdown', { client: this.client });
|
|
138
|
+
|
|
139
|
+
this.client.destroy();
|
|
140
|
+
this.client.removeAllListeners();
|
|
141
|
+
|
|
142
|
+
this.client = new Client({ intents: this._resolveIntents() });
|
|
143
|
+
this._bindEvents();
|
|
144
|
+
|
|
145
|
+
await this.client.login(this.config.token);
|
|
146
|
+
|
|
147
|
+
console.log(`[runtime] Restarted in ${Date.now() - t0}ms`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async stop() {
|
|
151
|
+
console.log('[runtime] Shutting down...');
|
|
152
|
+
await this.pluginManager.emitHook('bot_shutdown', { client: this.client });
|
|
153
|
+
if (this.client) {
|
|
154
|
+
this.client.destroy();
|
|
155
|
+
}
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getUptime() {
|
|
160
|
+
return Date.now() - this.startTime;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getCommand(name) {
|
|
164
|
+
return this.pluginManager.getCommand(name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getAllCommands() {
|
|
168
|
+
return this.pluginManager.getAllCommands();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getPluginConfig(pluginName) {
|
|
172
|
+
return this.pluginManager.getPluginConfig(pluginName);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getPluginAPI(pluginName) {
|
|
176
|
+
return this.pluginManager.getPluginAPI(pluginName);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = SpiralRuntime;
|