spiralcord-full 2.1.0 → 2.2.7
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 +273 -350
- package/package.json +4 -2
- package/src/plugins/manager.js +162 -34
- package/src/runtime.js +157 -22
- package/src/test.js +85 -63
- package/src/types/index.d.ts +100 -0
- package/src/web/server.js +69 -41
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spiralcord-full",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.7",
|
|
4
4
|
"description": "Spiralcord Full - Discord Bot Runtime with Voice, Music, AI, Database & Image Support",
|
|
5
5
|
"main": "src/index.js",
|
|
6
|
+
"types": "src/types/index.d.ts",
|
|
6
7
|
"bin": {
|
|
7
8
|
"spiral": "./bin/spiral.js"
|
|
8
9
|
},
|
|
@@ -43,7 +44,8 @@
|
|
|
43
44
|
"dotenv": "^16.3.0",
|
|
44
45
|
"chalk": "^4.1.2",
|
|
45
46
|
"ms": "^2.1.3",
|
|
46
|
-
"uuid": "^9.0.0"
|
|
47
|
+
"uuid": "^9.0.0",
|
|
48
|
+
"chokidar": "^3.6.0"
|
|
47
49
|
},
|
|
48
50
|
"engines": {
|
|
49
51
|
"node": ">=16.0.0"
|
package/src/plugins/manager.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs').promises;
|
|
|
2
2
|
const fsSync = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const EventEmitter = require('events');
|
|
5
|
+
const { EmbedBuilder } = require('discord.js');
|
|
5
6
|
|
|
6
7
|
class SpiralPluginManager extends EventEmitter {
|
|
7
8
|
constructor(runtime) {
|
|
@@ -14,6 +15,7 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
14
15
|
this.loadOrder = [];
|
|
15
16
|
this.maxListeners = 50;
|
|
16
17
|
this._cache = new Map();
|
|
18
|
+
this._hookCleanups = [];
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
async loadPlugins(pluginsDir) {
|
|
@@ -23,14 +25,18 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
this.removeAllListeners();
|
|
29
|
+
this._hookCleanups.forEach(cleanup => cleanup());
|
|
30
|
+
this._hookCleanups = [];
|
|
31
|
+
|
|
32
|
+
const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
|
|
33
|
+
const pluginFolders = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
27
34
|
const manifests = [];
|
|
28
35
|
|
|
29
36
|
const manifestPromises = pluginFolders.map(async (folder) => {
|
|
30
37
|
const pluginPath = path.resolve(pluginsDir, folder);
|
|
31
38
|
const manifestPath = path.join(pluginPath, 'plugin.json');
|
|
32
39
|
try {
|
|
33
|
-
await fs.access(manifestPath);
|
|
34
40
|
const data = await fs.readFile(manifestPath, 'utf-8');
|
|
35
41
|
return { folder, pluginPath, manifest: JSON.parse(data) };
|
|
36
42
|
} catch {
|
|
@@ -45,10 +51,13 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
45
51
|
|
|
46
52
|
const sorted = this.resolveDependencies(manifests);
|
|
47
53
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
for (const { folder, pluginPath, manifest } of sorted) {
|
|
55
|
+
try {
|
|
56
|
+
await this.loadPlugin(folder, pluginPath, manifest);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`[manager] Failed to load ${manifest.name}:`, error.message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
resolveDependencies(manifests) {
|
|
@@ -64,7 +73,7 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
64
73
|
const visit = (name) => {
|
|
65
74
|
if (visited.has(name)) return;
|
|
66
75
|
if (visiting.has(name)) {
|
|
67
|
-
console.error(`[manager] Circular dependency: ${name}`);
|
|
76
|
+
console.error(`[manager] Circular dependency detected: ${name}`);
|
|
68
77
|
return;
|
|
69
78
|
}
|
|
70
79
|
visiting.add(name);
|
|
@@ -72,6 +81,9 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
72
81
|
const m = manifestMap.get(name);
|
|
73
82
|
if (m && m.manifest.dependencies) {
|
|
74
83
|
for (const dep of Object.keys(m.manifest.dependencies)) {
|
|
84
|
+
if (!manifestMap.has(dep)) {
|
|
85
|
+
console.warn(`[manager] Missing dependency "${dep}" for ${name}`);
|
|
86
|
+
}
|
|
75
87
|
visit(dep);
|
|
76
88
|
}
|
|
77
89
|
}
|
|
@@ -104,8 +116,12 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
104
116
|
this.pluginConfigs.set(manifest.name, config);
|
|
105
117
|
this.plugins.set(manifest.name, { manifest, module: pluginModule, path: pluginPath });
|
|
106
118
|
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
try {
|
|
120
|
+
if (pluginModule.init) {
|
|
121
|
+
await pluginModule.init(config, this.runtime);
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`[manager] Init failed for ${manifest.name}:`, error.message);
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
if (pluginModule.commands) {
|
|
@@ -122,13 +138,15 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
122
138
|
|
|
123
139
|
if (pluginModule.hooks) {
|
|
124
140
|
for (const [hookName, handler] of Object.entries(pluginModule.hooks)) {
|
|
125
|
-
|
|
141
|
+
const listener = async (payload) => {
|
|
126
142
|
try {
|
|
127
143
|
await handler(payload, this.runtime);
|
|
128
144
|
} catch (error) {
|
|
129
145
|
console.error(`[manager] Hook error ${hookName} from ${manifest.name}:`, error.message);
|
|
130
146
|
}
|
|
131
|
-
}
|
|
147
|
+
};
|
|
148
|
+
this.on(hookName, listener);
|
|
149
|
+
this._hookCleanups.push(() => this.removeListener(hookName, listener));
|
|
132
150
|
}
|
|
133
151
|
}
|
|
134
152
|
|
|
@@ -143,15 +161,24 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
143
161
|
async _loadModule(indexPath, dslPath, manifest) {
|
|
144
162
|
try {
|
|
145
163
|
await fs.access(indexPath);
|
|
146
|
-
|
|
164
|
+
const resolved = require.resolve(indexPath);
|
|
165
|
+
delete require.cache[resolved];
|
|
147
166
|
return require(indexPath);
|
|
148
|
-
} catch {
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.code !== 'ENOENT') {
|
|
169
|
+
console.error(`[manager] Failed to load ${indexPath}:`, error.message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
149
172
|
|
|
150
173
|
try {
|
|
151
174
|
await fs.access(dslPath);
|
|
152
175
|
const dslContent = await fs.readFile(dslPath, 'utf-8');
|
|
153
176
|
return this.compileDSL(dslContent, manifest);
|
|
154
|
-
} catch {
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error.code !== 'ENOENT') {
|
|
179
|
+
console.error(`[manager] Failed to load ${dslPath}:`, error.message);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
155
182
|
|
|
156
183
|
return null;
|
|
157
184
|
}
|
|
@@ -167,20 +194,26 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
167
194
|
if (existing) {
|
|
168
195
|
existing.handler = cmd.execute || cmd;
|
|
169
196
|
existing.description = cmd.description || existing.description;
|
|
197
|
+
registrations.sort((a, b) => b.priority - a.priority);
|
|
170
198
|
return;
|
|
171
199
|
}
|
|
172
200
|
|
|
173
|
-
const manifest = this.plugins.get(pluginName)?.manifest;
|
|
174
|
-
const collisionPolicy = manifest?.collision_policy || 'last-wins';
|
|
175
|
-
|
|
176
201
|
if (registrations.length > 0) {
|
|
177
202
|
const conflict = registrations[0];
|
|
203
|
+
const conflictManifest = this.plugins.get(conflict.plugin)?.manifest;
|
|
204
|
+
const policy = conflictManifest?.collision_policy || 'last-wins';
|
|
205
|
+
|
|
178
206
|
console.warn(`[manager] Command "${name}" collision: ${conflict.plugin} vs ${pluginName}`);
|
|
179
207
|
|
|
180
|
-
if (
|
|
208
|
+
if (policy === 'error') {
|
|
181
209
|
console.error(`[manager] Refusing ${pluginName}: "${name}" already registered`);
|
|
182
210
|
return;
|
|
183
211
|
}
|
|
212
|
+
|
|
213
|
+
if (policy === 'first-wins') {
|
|
214
|
+
console.log(`[manager] Keeping ${conflict.plugin}'s "${name}" (first-wins)`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
184
217
|
}
|
|
185
218
|
|
|
186
219
|
registrations.push({
|
|
@@ -234,7 +267,7 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
234
267
|
}
|
|
235
268
|
|
|
236
269
|
try {
|
|
237
|
-
await cmd.handler(message, args, this.runtime);
|
|
270
|
+
await cmd.handler(message, args || [], this.runtime);
|
|
238
271
|
return true;
|
|
239
272
|
} catch (error) {
|
|
240
273
|
console.error(`[manager] Error executing ${name}:`, error);
|
|
@@ -244,7 +277,14 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
244
277
|
}
|
|
245
278
|
|
|
246
279
|
async emitHook(eventName, payload) {
|
|
247
|
-
this.
|
|
280
|
+
const listeners = this.listeners(eventName);
|
|
281
|
+
for (const listener of listeners) {
|
|
282
|
+
try {
|
|
283
|
+
await listener(payload);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error(`[manager] Hook error ${eventName}:`, error.message);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
248
288
|
}
|
|
249
289
|
|
|
250
290
|
async emitHookSerial(eventName, payload) {
|
|
@@ -313,6 +353,7 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
313
353
|
compileDSL(dslContent, manifest) {
|
|
314
354
|
const lines = dslContent.split('\n');
|
|
315
355
|
const commands = {};
|
|
356
|
+
const slashCommands = [];
|
|
316
357
|
|
|
317
358
|
let currentBlock = null;
|
|
318
359
|
let blockLines = [];
|
|
@@ -321,21 +362,30 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
321
362
|
const trimmed = line.trim();
|
|
322
363
|
if (trimmed === '' || trimmed.startsWith('#')) continue;
|
|
323
364
|
|
|
324
|
-
if (trimmed.startsWith('COMMAND ')) {
|
|
365
|
+
if (trimmed.startsWith('COMMAND ') || trimmed.startsWith('SLASH_COMMAND ')) {
|
|
325
366
|
if (currentBlock) {
|
|
326
|
-
this._processDSLBlock(currentBlock, blockLines, commands);
|
|
367
|
+
this._processDSLBlock(currentBlock, blockLines, commands, slashCommands);
|
|
327
368
|
blockLines = [];
|
|
328
369
|
}
|
|
329
|
-
const
|
|
370
|
+
const isSlash = trimmed.startsWith('SLASH_COMMAND ');
|
|
371
|
+
const pattern = isSlash ? /SLASH_COMMAND\s+(\S+)\s+"([^"]+)"/ : /COMMAND\s+(\S+)\s+"([^"]+)"/;
|
|
372
|
+
const match = trimmed.match(pattern);
|
|
330
373
|
if (match) {
|
|
331
|
-
currentBlock = {
|
|
374
|
+
currentBlock = {
|
|
375
|
+
type: 'command', name: match[1], description: match[2],
|
|
376
|
+
aliases: [], cooldown: 0, requiresArgs: null, requiresPermission: null,
|
|
377
|
+
response: null, responsePool: null, embed: null, slash: isSlash,
|
|
378
|
+
buttons: [], selects: [], dbOps: [], selectPlaceholder: null
|
|
379
|
+
};
|
|
380
|
+
} else {
|
|
381
|
+
currentBlock = null;
|
|
332
382
|
}
|
|
333
383
|
continue;
|
|
334
384
|
}
|
|
335
385
|
|
|
336
386
|
if (trimmed === 'END') {
|
|
337
387
|
if (currentBlock) {
|
|
338
|
-
this._processDSLBlock(currentBlock, blockLines, commands);
|
|
388
|
+
this._processDSLBlock(currentBlock, blockLines, commands, slashCommands);
|
|
339
389
|
currentBlock = null;
|
|
340
390
|
blockLines = [];
|
|
341
391
|
}
|
|
@@ -345,15 +395,30 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
345
395
|
if (currentBlock) blockLines.push(trimmed);
|
|
346
396
|
}
|
|
347
397
|
|
|
348
|
-
|
|
398
|
+
const init = async (config, runtime) => {};
|
|
399
|
+
|
|
400
|
+
if (slashCommands.length > 0) {
|
|
401
|
+
init._slashCommands = slashCommands;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return { commands, init };
|
|
349
405
|
}
|
|
350
406
|
|
|
351
|
-
_processDSLBlock(block, lines, commands) {
|
|
407
|
+
_processDSLBlock(block, lines, commands, slashCommands) {
|
|
352
408
|
if (block.type !== 'command') return;
|
|
353
409
|
|
|
410
|
+
const dslKeywords = [
|
|
411
|
+
'ALIASES', 'COOLDOWN', 'REQUIRES_ARGS', 'REQUIRES_PERMISSION',
|
|
412
|
+
'RESPONSE', 'RESPONSE_POOL', 'EMBED', 'TITLE', 'DESCRIPTION',
|
|
413
|
+
'BUTTON', 'SELECT', 'DB_SET', 'DB_GET', 'DB_ADD', 'DB_DELETE', 'DB_RESPONSE'
|
|
414
|
+
];
|
|
415
|
+
|
|
354
416
|
for (const line of lines) {
|
|
355
417
|
if (line.startsWith('ALIASES ')) block.aliases = line.slice(8).split(',').map(a => a.trim());
|
|
356
|
-
else if (line.startsWith('COOLDOWN '))
|
|
418
|
+
else if (line.startsWith('COOLDOWN ')) {
|
|
419
|
+
const val = parseInt(line.slice(9), 10);
|
|
420
|
+
block.cooldown = isNaN(val) ? 0 : val * 1000;
|
|
421
|
+
}
|
|
357
422
|
else if (line.startsWith('REQUIRES_ARGS ')) block.requiresArgs = line.slice(14).replace(/^["']|["']$/g, '');
|
|
358
423
|
else if (line.startsWith('REQUIRES_PERMISSION ')) block.requiresPermission = line.slice(20);
|
|
359
424
|
else if (line.startsWith('RESPONSE ')) block.response = line.slice(9).replace(/^["']|["']$/g, '');
|
|
@@ -364,7 +429,41 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
364
429
|
}
|
|
365
430
|
else if (line.startsWith('TITLE ')) { if (block.embed) block.embed.title = line.slice(6).replace(/^["']|["']$/g, ''); }
|
|
366
431
|
else if (line.startsWith('DESCRIPTION ')) { if (block.embed) block.embed.description = line.slice(12).replace(/^["']|["']$/g, ''); }
|
|
367
|
-
else if (
|
|
432
|
+
else if (line.startsWith('BUTTON ')) {
|
|
433
|
+
const bMatch = line.match(/BUTTON\s+"([^"]+)"\s+STYLE\s+(\w+)\s+ACTION\s+(\S+)/);
|
|
434
|
+
if (bMatch) block.buttons.push({ label: bMatch[1], style: bMatch[2], action: bMatch[3] });
|
|
435
|
+
}
|
|
436
|
+
else if (line.startsWith('SELECT ')) {
|
|
437
|
+
const sMatch = line.match(/SELECT\s+"([^"]+)"\s+OPTION\s+"([^"]+)"\s+VALUE\s+"([^"]+)"/);
|
|
438
|
+
if (sMatch) {
|
|
439
|
+
if (!block.selectPlaceholder) block.selectPlaceholder = sMatch[1];
|
|
440
|
+
block.selects.push({ label: sMatch[2], value: sMatch[3] });
|
|
441
|
+
} else {
|
|
442
|
+
const sPlaceholder = line.match(/SELECT\s+"([^"]+)"/);
|
|
443
|
+
if (sPlaceholder) block.selectPlaceholder = sPlaceholder[1];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else if (line.startsWith('DB_SET ')) {
|
|
447
|
+
const dbMatch = line.match(/DB_SET\s+(\S+)\s+(.+)/);
|
|
448
|
+
if (dbMatch) block.dbOps.push({ op: 'set', key: dbMatch[1], value: dbMatch[2].replace(/^["']|["']$/g, '') });
|
|
449
|
+
}
|
|
450
|
+
else if (line.startsWith('DB_GET ')) {
|
|
451
|
+
const dbMatch = line.match(/DB_GET\s+(\S+)/);
|
|
452
|
+
if (dbMatch) block.dbOps.push({ op: 'get', key: dbMatch[1] });
|
|
453
|
+
}
|
|
454
|
+
else if (line.startsWith('DB_ADD ')) {
|
|
455
|
+
const dbMatch = line.match(/DB_ADD\s+(\S+)\s+(-?\d+)/);
|
|
456
|
+
if (dbMatch) block.dbOps.push({ op: 'add', key: dbMatch[1], amount: parseInt(dbMatch[2], 10) });
|
|
457
|
+
}
|
|
458
|
+
else if (line.startsWith('DB_DELETE ')) {
|
|
459
|
+
const dbMatch = line.match(/DB_DELETE\s+(\S+)/);
|
|
460
|
+
if (dbMatch) block.dbOps.push({ op: 'delete', key: dbMatch[1] });
|
|
461
|
+
}
|
|
462
|
+
else if (line.startsWith('DB_RESPONSE ')) {
|
|
463
|
+
const dbMatch = line.match(/DB_RESPONSE\s+"([^"]+)"/);
|
|
464
|
+
if (dbMatch) block.dbOps.push({ op: 'response', key: dbMatch[1] });
|
|
465
|
+
}
|
|
466
|
+
else if (block.responsePool !== null && !dslKeywords.some(kw => line.startsWith(kw))) {
|
|
368
467
|
block.responsePool.push(line.replace(/^["']|["']$/g, ''));
|
|
369
468
|
}
|
|
370
469
|
}
|
|
@@ -373,23 +472,45 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
373
472
|
const manager = this;
|
|
374
473
|
const handler = async (message, args, runtime) => {
|
|
375
474
|
if (blockRef.requiresArgs && (!args || args.length === 0)) return message.reply(blockRef.requiresArgs);
|
|
376
|
-
if (blockRef.requiresPermission && !message.member
|
|
475
|
+
if (blockRef.requiresPermission && message.member && !message.member.permissions.has(blockRef.requiresPermission)) {
|
|
377
476
|
return message.reply(`You need \`${blockRef.requiresPermission}\` permission.`);
|
|
378
477
|
}
|
|
379
478
|
|
|
380
479
|
const ctxMap = new Map([
|
|
381
480
|
['user.id', message.author.id], ['user.name', message.author.username],
|
|
382
481
|
['channel.id', message.channel.id], ['guild.id', message.guild?.id || 'dm'],
|
|
383
|
-
['guild.name', message.guild?.name || 'DM'], ['args', args.join(' ')]
|
|
482
|
+
['guild.name', message.guild?.name || 'DM'], ['args', (args || []).join(' ')]
|
|
384
483
|
]);
|
|
385
484
|
|
|
485
|
+
for (const dbOp of blockRef.dbOps) {
|
|
486
|
+
const key = manager._resolveDSLVars(dbOp.key, ctxMap);
|
|
487
|
+
const userId = message.author.id;
|
|
488
|
+
const fullKey = `${userId}.${key}`;
|
|
489
|
+
|
|
490
|
+
if (dbOp.op === 'set') {
|
|
491
|
+
const val = manager._resolveDSLVars(dbOp.value, ctxMap);
|
|
492
|
+
const num = parseFloat(val);
|
|
493
|
+
runtime.db.set(fullKey, isNaN(num) ? val : num);
|
|
494
|
+
} else if (dbOp.op === 'get') {
|
|
495
|
+
const val = runtime.db.get(fullKey);
|
|
496
|
+
ctxMap.set('db_value', val !== null ? String(val) : '0');
|
|
497
|
+
} else if (dbOp.op === 'add') {
|
|
498
|
+
const newVal = runtime.db.add(fullKey, dbOp.amount);
|
|
499
|
+
ctxMap.set('db_value', String(newVal));
|
|
500
|
+
} else if (dbOp.op === 'delete') {
|
|
501
|
+
runtime.db.delete(fullKey);
|
|
502
|
+
} else if (dbOp.op === 'response') {
|
|
503
|
+
const val = runtime.db.get(fullKey);
|
|
504
|
+
ctxMap.set('db_response', val !== null ? String(val) : '0');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
386
508
|
if (blockRef.response) return message.reply(manager._resolveDSLVars(blockRef.response, ctxMap));
|
|
387
509
|
if (blockRef.responsePool && blockRef.responsePool.length > 0) {
|
|
388
510
|
const response = blockRef.responsePool[Math.floor(Math.random() * blockRef.responsePool.length)];
|
|
389
511
|
return message.reply(manager._resolveDSLVars(response, ctxMap));
|
|
390
512
|
}
|
|
391
513
|
if (blockRef.embed) {
|
|
392
|
-
const { EmbedBuilder } = require('discord.js');
|
|
393
514
|
const embed = new EmbedBuilder()
|
|
394
515
|
.setColor(manager._resolveDSLVars(blockRef.embed.color, ctxMap))
|
|
395
516
|
.setTitle(manager._resolveDSLVars(blockRef.embed.title || '', ctxMap))
|
|
@@ -398,10 +519,17 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
398
519
|
}
|
|
399
520
|
};
|
|
400
521
|
|
|
401
|
-
commands[blockRef.name] = {
|
|
522
|
+
commands[blockRef.name] = {
|
|
523
|
+
description: blockRef.description, execute: handler,
|
|
524
|
+
slash: blockRef.slash || false
|
|
525
|
+
};
|
|
402
526
|
for (const alias of blockRef.aliases) {
|
|
403
527
|
commands[alias] = { description: blockRef.description, execute: handler, slash: false };
|
|
404
528
|
}
|
|
529
|
+
|
|
530
|
+
if (blockRef.slash) {
|
|
531
|
+
slashCommands.push({ name: blockRef.name, description: blockRef.description });
|
|
532
|
+
}
|
|
405
533
|
}
|
|
406
534
|
|
|
407
535
|
_resolveDSLVars(text, ctxMap) {
|
|
@@ -410,4 +538,4 @@ class SpiralPluginManager extends EventEmitter {
|
|
|
410
538
|
}
|
|
411
539
|
}
|
|
412
540
|
|
|
413
|
-
module.exports = SpiralPluginManager;
|
|
541
|
+
module.exports = SpiralPluginManager;
|
package/src/runtime.js
CHANGED
|
@@ -3,13 +3,117 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const SpiralPluginManager = require('./plugins/manager');
|
|
5
5
|
|
|
6
|
+
class SpiralDB {
|
|
7
|
+
constructor(filePath) {
|
|
8
|
+
this.filePath = filePath;
|
|
9
|
+
this.data = {};
|
|
10
|
+
this._load();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_load() {
|
|
14
|
+
try {
|
|
15
|
+
if (fs.existsSync(this.filePath)) {
|
|
16
|
+
this.data = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
|
|
17
|
+
}
|
|
18
|
+
} catch { this.data = {}; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_save() {
|
|
22
|
+
try {
|
|
23
|
+
const dir = path.dirname(this.filePath);
|
|
24
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('[db] Save error:', e.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get(key, defaultValue = null) {
|
|
32
|
+
if (key == null || typeof key !== 'string') return defaultValue;
|
|
33
|
+
const keys = key.split('.');
|
|
34
|
+
let val = this.data;
|
|
35
|
+
for (const k of keys) {
|
|
36
|
+
if (val === null || val === undefined || typeof val !== 'object') return defaultValue;
|
|
37
|
+
val = val[k];
|
|
38
|
+
}
|
|
39
|
+
return val !== undefined ? val : defaultValue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
set(key, value) {
|
|
43
|
+
if (key == null || typeof key !== 'string') return value;
|
|
44
|
+
const keys = key.split('.');
|
|
45
|
+
let obj = this.data;
|
|
46
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
47
|
+
if (obj[keys[i]] !== undefined && typeof obj[keys[i]] !== 'object') {
|
|
48
|
+
console.warn(`[db] Overwriting non-object at key "${keys.slice(0, i + 1).join('.')}"`);
|
|
49
|
+
}
|
|
50
|
+
if (!obj[keys[i]] || typeof obj[keys[i]] !== 'object') obj[keys[i]] = {};
|
|
51
|
+
obj = obj[keys[i]];
|
|
52
|
+
}
|
|
53
|
+
obj[keys[keys.length - 1]] = value;
|
|
54
|
+
this._save();
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
add(key, amount) {
|
|
59
|
+
if (typeof amount !== 'number' || isNaN(amount)) {
|
|
60
|
+
console.warn(`[db] add() received non-numeric amount: ${amount}`);
|
|
61
|
+
amount = 0;
|
|
62
|
+
}
|
|
63
|
+
const current = this.get(key, 0);
|
|
64
|
+
const newVal = (typeof current === 'number' ? current : 0) + amount;
|
|
65
|
+
this.set(key, newVal);
|
|
66
|
+
return newVal;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
delete(key) {
|
|
70
|
+
if (key == null || typeof key !== 'string') return false;
|
|
71
|
+
const keys = key.split('.');
|
|
72
|
+
let obj = this.data;
|
|
73
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
74
|
+
if (!obj[keys[i]] || typeof obj[keys[i]] !== 'object') return false;
|
|
75
|
+
obj = obj[keys[i]];
|
|
76
|
+
}
|
|
77
|
+
const existed = keys[keys.length - 1] in obj;
|
|
78
|
+
delete obj[keys[keys.length - 1]];
|
|
79
|
+
this._save();
|
|
80
|
+
return existed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
has(key) {
|
|
84
|
+
if (key == null || typeof key !== 'string') return false;
|
|
85
|
+
const keys = key.split('.');
|
|
86
|
+
let val = this.data;
|
|
87
|
+
for (const k of keys) {
|
|
88
|
+
if (val === null || val === undefined || typeof val !== 'object') return false;
|
|
89
|
+
if (!(k in val)) return false;
|
|
90
|
+
val = val[k];
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
all() {
|
|
96
|
+
return { ...this.data };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
clear() {
|
|
100
|
+
this.data = {};
|
|
101
|
+
this._save();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
6
105
|
class SpiralRuntime {
|
|
7
106
|
constructor(config) {
|
|
8
107
|
this.config = config || {};
|
|
108
|
+
if (!this.config.token || typeof this.config.token !== 'string') {
|
|
109
|
+
throw new Error('Bot token is required. Set it in spiral.json');
|
|
110
|
+
}
|
|
9
111
|
this.client = null;
|
|
10
112
|
this.pluginManager = new SpiralPluginManager(this);
|
|
11
113
|
this.startTime = Date.now();
|
|
12
114
|
this._pluginsDir = path.join(process.cwd(), 'plugins');
|
|
115
|
+
this._unhandledHandler = null;
|
|
116
|
+
this.db = new SpiralDB(path.join(process.cwd(), 'data.json'));
|
|
13
117
|
}
|
|
14
118
|
|
|
15
119
|
async start() {
|
|
@@ -31,22 +135,27 @@ class SpiralRuntime {
|
|
|
31
135
|
|
|
32
136
|
_resolveIntents() {
|
|
33
137
|
const intents = this.config.intents || ['Guilds', 'GuildMessages', 'MessageContent'];
|
|
138
|
+
const map = {
|
|
139
|
+
'Guilds': GatewayIntentBits.Guilds,
|
|
140
|
+
'GuildMessages': GatewayIntentBits.GuildMessages,
|
|
141
|
+
'MessageContent': GatewayIntentBits.MessageContent,
|
|
142
|
+
'GuildMembers': GatewayIntentBits.GuildMembers,
|
|
143
|
+
'GuildVoiceStates': GatewayIntentBits.GuildVoiceStates,
|
|
144
|
+
'DirectMessages': GatewayIntentBits.DirectMessages,
|
|
145
|
+
'GuildPresences': GatewayIntentBits.GuildPresences
|
|
146
|
+
};
|
|
34
147
|
return intents.map(i => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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;
|
|
148
|
+
if (map[i] !== undefined) return map[i];
|
|
149
|
+
console.warn(`[runtime] Unknown intent "${i}", defaulting to Guilds`);
|
|
150
|
+
return GatewayIntentBits.Guilds;
|
|
45
151
|
});
|
|
46
152
|
}
|
|
47
153
|
|
|
48
154
|
_bindEvents() {
|
|
49
155
|
this.client.removeAllListeners();
|
|
156
|
+
if (this._unhandledHandler) {
|
|
157
|
+
process.removeListener('unhandledRejection', this._unhandledHandler);
|
|
158
|
+
}
|
|
50
159
|
|
|
51
160
|
this.client.on(Events.ClientReady, async () => {
|
|
52
161
|
console.log(`[runtime] Ready as ${this.client.user.tag} | ${this.client.guilds.cache.size} servers | ${this.client.users.cache.size} users`);
|
|
@@ -106,6 +215,12 @@ class SpiralRuntime {
|
|
|
106
215
|
});
|
|
107
216
|
|
|
108
217
|
this.client.on(Events.MessageReactionAdd, async (reaction, user) => {
|
|
218
|
+
if (reaction.partial) {
|
|
219
|
+
try { await reaction.fetch(); } catch { return; }
|
|
220
|
+
}
|
|
221
|
+
if (user.partial) {
|
|
222
|
+
try { await user.fetch(); } catch { return; }
|
|
223
|
+
}
|
|
109
224
|
await this.pluginManager.emitHook('reaction_add', {
|
|
110
225
|
reaction,
|
|
111
226
|
user,
|
|
@@ -114,6 +229,12 @@ class SpiralRuntime {
|
|
|
114
229
|
});
|
|
115
230
|
|
|
116
231
|
this.client.on(Events.MessageReactionRemove, async (reaction, user) => {
|
|
232
|
+
if (reaction.partial) {
|
|
233
|
+
try { await reaction.fetch(); } catch { return; }
|
|
234
|
+
}
|
|
235
|
+
if (user.partial) {
|
|
236
|
+
try { await user.fetch(); } catch { return; }
|
|
237
|
+
}
|
|
117
238
|
await this.pluginManager.emitHook('reaction_remove', {
|
|
118
239
|
reaction,
|
|
119
240
|
user,
|
|
@@ -125,35 +246,49 @@ class SpiralRuntime {
|
|
|
125
246
|
console.error('[runtime] Error:', error.message);
|
|
126
247
|
});
|
|
127
248
|
|
|
128
|
-
|
|
249
|
+
this._unhandledHandler = (error) => {
|
|
129
250
|
console.error('[runtime] Unhandled:', error);
|
|
130
|
-
}
|
|
251
|
+
};
|
|
252
|
+
process.on('unhandledRejection', this._unhandledHandler);
|
|
131
253
|
}
|
|
132
254
|
|
|
133
255
|
async restart() {
|
|
134
256
|
const t0 = Date.now();
|
|
135
257
|
console.log('[runtime] Restarting...');
|
|
136
258
|
|
|
137
|
-
|
|
259
|
+
try {
|
|
260
|
+
await this.pluginManager.emitHook('bot_shutdown', { client: this.client });
|
|
261
|
+
} catch {}
|
|
138
262
|
|
|
139
|
-
|
|
140
|
-
|
|
263
|
+
try {
|
|
264
|
+
if (this.client) this.client.destroy();
|
|
265
|
+
} catch {}
|
|
266
|
+
if (this.client) this.client.removeAllListeners();
|
|
141
267
|
|
|
142
268
|
this.client = new Client({ intents: this._resolveIntents() });
|
|
143
269
|
this._bindEvents();
|
|
144
270
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
271
|
+
try {
|
|
272
|
+
await this.pluginManager.loadPlugins(this._pluginsDir);
|
|
273
|
+
await this.client.login(this.config.token);
|
|
274
|
+
console.log(`[runtime] Restarted in ${Date.now() - t0}ms`);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error('[runtime] Restart failed:', error.message);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
148
279
|
}
|
|
149
280
|
|
|
150
281
|
async stop() {
|
|
151
282
|
console.log('[runtime] Shutting down...');
|
|
152
|
-
|
|
283
|
+
try {
|
|
284
|
+
await this.pluginManager.emitHook('bot_shutdown', { client: this.client });
|
|
285
|
+
} catch {}
|
|
153
286
|
if (this.client) {
|
|
154
|
-
this.client.destroy();
|
|
287
|
+
try { this.client.destroy(); } catch {}
|
|
288
|
+
}
|
|
289
|
+
if (this._unhandledHandler) {
|
|
290
|
+
process.removeListener('unhandledRejection', this._unhandledHandler);
|
|
155
291
|
}
|
|
156
|
-
process.exit(0);
|
|
157
292
|
}
|
|
158
293
|
|
|
159
294
|
getUptime() {
|
|
@@ -177,4 +312,4 @@ class SpiralRuntime {
|
|
|
177
312
|
}
|
|
178
313
|
}
|
|
179
314
|
|
|
180
|
-
module.exports = SpiralRuntime;
|
|
315
|
+
module.exports = SpiralRuntime;
|