kythia-core 0.9.4-beta.3 → 0.10.1-beta

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/src/Kythia.js CHANGED
@@ -1,24 +1,24 @@
1
1
  /**
2
- * 🤖 Main Kythia Entrypoint
2
+ * 🌸 Kythia Core Application (The Orchestrator)
3
3
  *
4
4
  * @file src/Kythia.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.4-beta.3
7
+ * @version 0.10.0-beta
8
8
  *
9
9
  * @description
10
- * This file contains the main Bot class - acting as an orchestrator (CEO) that
11
- * initializes and coordinates specialized managers for different responsibilities.
10
+ * The heart of the application lifecycle. This class acts as the central
11
+ * Dependency Injection (DI) container and orchestrator for all subsystems.
12
+ * It manages the startup sequence, module loading, and graceful shutdown procedures.
12
13
  *
13
- * ✨ Core Features:
14
- * - Orchestrates AddonManager, InteractionManager, EventManager, and ShutdownManager
15
- * - REST API setup for command deployment
16
- * - Integration with client and database
17
- * - Manages dependencies through container pattern
14
+ * ✨ Core Responsibilities:
15
+ * - Lifecycle Management: Bootstrapping, Running, and Terminating the bot safely.
16
+ * - IoC Container: Central hub for Services, Managers, and Database connections.
17
+ * - Addon Orchestration: Loading and initializing modular addons dynamically.
18
+ * - Event Dispatching: Routes Gateway events to appropriate handlers.
18
19
  */
19
20
 
20
21
  const { REST, Routes, Collection } = require('discord.js');
21
- const KythiaORM = require('./database/KythiaORM');
22
22
  const KythiaClient = require('./KythiaClient');
23
23
  const Sentry = require('@sentry/node');
24
24
  const figlet = require('figlet');
@@ -28,435 +28,527 @@ const ShutdownManager = require('./managers/ShutdownManager');
28
28
  const AddonManager = require('./managers/AddonManager');
29
29
  const EventManager = require('./managers/EventManager');
30
30
 
31
+ const KythiaMigrator = require('./database/KythiaMigrator');
32
+ const ModelLoader = require('./database/ModelLoader');
33
+
31
34
  class Kythia {
32
- /**
33
- * 🏗️ Kythia Constructor
34
- * Initializes the Discord client, REST API, and dependency container.
35
- * Sets up manager instances (but doesn't start them yet).
36
- */
37
- constructor({ config, logger, redis, sequelize, translator, models, helpers, utils, appRoot }) {
38
- const missingDeps = [];
39
- if (!config) missingDeps.push('config');
40
- if (!logger) missingDeps.push('logger');
41
- if (!translator) {
42
- missingDeps.push('translator');
43
- } else {
44
- if (!translator.t) missingDeps.push('translator.t');
45
- if (!translator.loadLocales) missingDeps.push('translator.loadLocales');
46
- }
47
- if (missingDeps.length > 0) {
48
- console.error(`FATAL: Missing required dependencies: ${missingDeps.join(', ')}.`);
49
- process.exit(1);
50
- }
51
- this.kythiaConfig = config;
52
- this.appRoot = appRoot || process.cwd();
53
-
54
- this.client = KythiaClient();
55
- this.client.commands = new Collection();
56
- this.rest = new REST({ version: '10' }).setToken(this.kythiaConfig.bot.token);
57
-
58
- this.models = models;
59
- this.helpers = helpers;
60
- this.utils = utils;
61
-
62
- this.redis = redis;
63
- this.sequelize = sequelize;
64
-
65
- this.logger = logger;
66
- this.translator = translator;
67
-
68
- this.container = {
69
- client: this.client,
70
- sequelize: this.sequelize,
71
- logger: this.logger,
72
- t: this.translator.t,
73
- redis: this.redis,
74
- kythiaConfig: this.kythiaConfig,
75
- translator: this.translator,
76
-
77
- models: this.models,
78
- helpers: this.helpers,
79
- appRoot: this.appRoot,
80
- };
81
-
82
- this.client.container = this.container;
83
- this.client.cooldowns = new Collection();
84
-
85
- this.dbReadyHooks = [];
86
- this.clientReadyHooks = [];
87
-
88
- this.addonManager = null;
89
- this.interactionManager = null;
90
- this.eventManager = null;
91
- this.shutdownManager = null;
92
- }
93
-
94
- /**
95
- * 🔍 Check Required Config
96
- * Checks if all required configurations are set.
97
- * Throws an error if any required config is missing.
98
- */
99
- _checkRequiredConfig() {
100
- const requiredBotConfig = [
101
- ['bot', 'token'],
102
- ['bot', 'clientId'],
103
- ['bot', 'clientSecret'],
104
- ];
105
- const missingBotConfigs = [];
106
- for (const pathArr of requiredBotConfig) {
107
- let value = this.kythiaConfig;
108
- for (const key of pathArr) {
109
- value = value?.[key];
110
- }
111
- if (value === undefined || value === null || value === '') {
112
- missingBotConfigs.push(pathArr.join('.'));
113
- }
114
- }
115
-
116
- if (!this.kythiaConfig.db) this.kythiaConfig.db = {};
117
-
118
- let driver = this.kythiaConfig.db.driver;
119
- if (!driver || driver === '') {
120
- this.kythiaConfig.db.driver = 'sqlite';
121
- driver = 'sqlite';
122
- this.logger.info('💡 DB driver not specified. Defaulting to: sqlite');
123
- } else {
124
- driver = driver.toLowerCase();
125
- this.kythiaConfig.db.driver = driver;
126
- }
127
-
128
- if (driver === 'sqlite') {
129
- if (!this.kythiaConfig.db.name || this.kythiaConfig.db.name === '') {
130
- this.kythiaConfig.db.name = 'kythiadata.sqlite';
131
- }
132
- }
133
-
134
- const requiredDbConfig = [
135
- ['db', 'driver'],
136
- ['db', 'name'],
137
- ];
138
-
139
- if (driver !== 'sqlite') {
140
- requiredDbConfig.push(['db', 'host'], ['db', 'port'], ['db', 'user'], ['db', 'pass']);
141
- }
142
-
143
- const missingDbConfigs = [];
144
- for (const pathArr of requiredDbConfig) {
145
- let value = this.kythiaConfig;
146
- for (const key of pathArr) {
147
- value = value?.[key];
148
- }
149
- if (value === undefined || value === null || value === '') {
150
- missingDbConfigs.push(pathArr.join('.'));
151
- }
152
- }
153
-
154
- const missingConfigs = missingBotConfigs.concat(missingDbConfigs);
155
-
156
- if (missingConfigs.length > 0) {
157
- this.logger.error(' Required configurations are not set:');
158
- for (const missing of missingConfigs) {
159
- this.logger.error(` - ${missing}`);
160
- }
161
- process.exit(1);
162
- }
163
-
164
- this.logger.info('✔️ All required configurations are set');
165
- }
166
-
167
- /**
168
- * 🔘 Register Button Handler
169
- * Delegates to AddonManager
170
- * @param {string} customId - The customId of the button
171
- * @param {Function} handler - The handler function to execute
172
- */
173
- registerButtonHandler(customId, handler) {
174
- if (this.addonManager) {
175
- this.addonManager.registerButtonHandler(customId, handler);
176
- }
177
- }
178
-
179
- /**
180
- * 📝 Register Modal Handler
181
- * Delegates to AddonManager
182
- * @param {string} customIdPrefix - The prefix of the modal customId
183
- * @param {Function} handler - The handler function to execute
184
- */
185
- registerModalHandler(customIdPrefix, handler) {
186
- if (this.addonManager) {
187
- this.addonManager.registerModalHandler(customIdPrefix, handler);
188
- }
189
- }
190
-
191
- /**
192
- * 🛡️ Validate License (Stub)
193
- * Placeholder for license validation logic for addons.
194
- * @param {string} licenseKey - The license key to validate
195
- * @param {string} addonName - The name of the addon
196
- * @returns {Promise<boolean>} Always returns true (stub)
197
- */
198
- async _validateLicense(licenseKey, addonName) {
199
- return true;
200
- }
201
-
202
- /**
203
- * 🚀 Deploy Commands to Discord
204
- * Deploys all registered slash commands to Discord using the REST API.
205
- * @param {Array} commands - Array of command data to deploy
206
- */
207
- async _deployCommands(commands) {
208
- if (!commands || commands.length === 0) {
209
- this.logger.info('No commands to deploy.');
210
- return;
211
- }
212
- try {
213
- const { slash, user, message } = this._getCommandCounts(commands);
214
- const clientId = this.kythiaConfig.bot.clientId;
215
- const devGuildId = this.kythiaConfig.bot.devGuildId;
216
-
217
- let deployType = '';
218
- if (this.kythiaConfig.env == 'dev' || this.kythiaConfig.env == 'development') {
219
- if (!devGuildId) {
220
- this.logger.warn('⚠️ devGuildId not set in config. Skipping guild command deployment.');
221
- return;
222
- }
223
- this.logger.info(`🟠 Deploying to GUILD ${devGuildId}...`);
224
- await this.rest.put(Routes.applicationGuildCommands(clientId, devGuildId), { body: commands });
225
- this.logger.info('✅ Guild commands deployed instantly!');
226
- deployType = `Guild (${devGuildId})`;
227
- } else {
228
- this.logger.info(`🟢 Deploying globally...`);
229
- await this.rest.put(Routes.applicationCommands(clientId), { body: commands });
230
- this.logger.info('✅ Global commands deployed successfully!');
231
- if (devGuildId) {
232
- this.logger.info(`🧹 Clearing old commands from dev guild: ${devGuildId}...`);
233
- try {
234
- await this.rest.put(Routes.applicationGuildCommands(clientId, devGuildId), { body: [] });
235
- this.logger.info('✅ Dev guild commands cleared successfully.');
236
- } catch (err) {
237
- this.logger.warn(`⚠️ Could not clear dev guild commands (maybe it was already clean): ${err.message}`);
238
- }
239
- }
240
- deployType = 'Global';
241
- }
242
-
243
- this.logger.info(`⭕ All Slash Commands: ${commands.length}`);
244
- this.logger.info(`⭕ Top Level Slash Commands: ${slash}`);
245
- this.logger.info(`⭕ User Context Menu: ${user}`);
246
- this.logger.info(`⭕ Message Context Menu: ${message}`);
247
- this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
248
- } catch (err) {
249
- this.logger.error('❌ Failed to deploy slash commands:', err);
250
- }
251
- }
252
-
253
- /**
254
- * 🧮 Count command types from JSON array
255
- * @param {Array} commandJsonArray - Array command data to be deployed
256
- * @returns {object} - Object containing counts { slash, user, message }
257
- * @private
258
- */
259
- _getCommandCounts(commandJsonArray) {
260
- const counts = { slash: 0, user: 0, message: 0 };
261
-
262
- if (!Array.isArray(commandJsonArray)) {
263
- this.logger.warn('commandJsonArray is not iterable. Returning zero counts.');
264
- return counts;
265
- }
266
-
267
- for (const cmd of commandJsonArray) {
268
- switch (cmd?.type) {
269
- case 1:
270
- case undefined:
271
- counts.slash++;
272
- break;
273
- case 2:
274
- counts.user++;
275
- break;
276
- case 3:
277
- counts.message++;
278
- break;
279
- }
280
- }
281
- return counts;
282
- }
283
-
284
- /**
285
- * Adds a callback to be executed when the database is ready.
286
- * The callback will be executed after all database models have been synchronized.
287
- * @param {function} callback - Callback to be executed when the database is ready
288
- */
289
- addDbReadyHook(callback) {
290
- this.dbReadyHooks.push(callback);
291
- }
292
-
293
- /**
294
- * Adds a callback to be executed when the client is ready.
295
- * @param {function} callback - Callback to be executed when the client is ready
296
- */
297
- addClientReadyHook(callback) {
298
- this.clientReadyHooks.push(callback);
299
- }
300
-
301
- /**
302
- * 🌸 Start the Kythia Bot
303
- * Main orchestration method that:
304
- * 1. Initializes Redis cache
305
- * 2. Creates and starts all managers
306
- * 3. Loads addons via AddonManager
307
- * 4. Initializes database
308
- * 5. Sets up interaction and event handlers
309
- * 6. Deploys commands
310
- * 7. Logs in to Discord
311
- */
312
- async start() {
313
- const version = require('../package.json').version;
314
- const clc = require('cli-color');
315
- const figletText = (text, opts) =>
316
- new Promise((resolve, reject) => {
317
- figlet.text(text, opts, (err, data) => {
318
- if (err) reject(err);
319
- else resolve(data);
320
- });
321
- });
322
-
323
- try {
324
- const data = await figletText('KYTHIA', {
325
- font: 'ANSI Shadow',
326
- horizontalLayout: 'full',
327
- verticalLayout: 'full',
328
- });
329
-
330
- const infoLines = [
331
- clc.cyan('Created by kenndeclouv'),
332
- clc.cyan('Discord Support: ') + clc.underline('https://dsc.gg/kythia'),
333
- clc.cyan('Official Documentation: ') + clc.underline('https://kythia.my.id/commands'),
334
- '',
335
- clc.cyanBright(`Kythia version: ${version}`),
336
- '',
337
- clc.yellowBright('Respect my work by not removing the credit'),
338
- ];
339
-
340
- const rawInfoLines = infoLines.map((line) => clc.strip(line));
341
- const infoMaxLen = Math.max(...rawInfoLines.map((l) => l.length));
342
- const pad = 8;
343
- const borderWidth = infoMaxLen + pad * 2;
344
- const borderChar = clc.cyanBright('═');
345
- const sideChar = clc.cyanBright('║');
346
- const topBorder = clc.cyanBright('╔' + borderChar.repeat(borderWidth) + '╗');
347
- const bottomBorder = clc.cyanBright('╚' + borderChar.repeat(borderWidth) + '╝');
348
- const emptyLine = sideChar + ' '.repeat(borderWidth) + sideChar;
349
-
350
- const figletLines = data.split('\n');
351
- const centeredFigletInBorder = figletLines
352
- .map((line) => {
353
- const rawLen = clc.strip(line).length;
354
- const spaces = ' '.repeat(Math.max(0, Math.floor((borderWidth - rawLen) / 2)));
355
- return sideChar + spaces + clc.cyanBright(line) + ' '.repeat(borderWidth - spaces.length - rawLen) + sideChar;
356
- })
357
- .join('\n');
358
-
359
- const centeredInfo = infoLines
360
- .map((line, idx) => {
361
- const raw = rawInfoLines[idx];
362
- const spaces = ' '.repeat(Math.floor((borderWidth - raw.length) / 2));
363
- return sideChar + spaces + line + ' '.repeat(borderWidth - spaces.length - raw.length) + sideChar;
364
- })
365
- .join('\n');
366
-
367
- console.log('\n' + topBorder);
368
- console.log(emptyLine);
369
- console.log(centeredFigletInBorder);
370
- console.log(emptyLine);
371
- console.log(centeredInfo);
372
- console.log(emptyLine);
373
- console.log(bottomBorder + '\n');
374
- } catch (err) {
375
- this.logger.error('❌ Failed to render figlet banner:', err);
376
- }
377
-
378
- this.logger.info('🚀 Starting kythia...');
379
-
380
- if (this.kythiaConfig.sentry.dsn) {
381
- Sentry.init({
382
- dsn: this.kythiaConfig.sentry.dsn,
383
- tracesSampleRate: 1.0,
384
- profilesSampleRate: 1.0,
385
- });
386
- this.logger.info('✔️ Sentry Error Tracking is ACTIVE');
387
- } else {
388
- this.logger.warn('🟠 Sentry DSN not found in config. Error tracking is INACTIVE.');
389
- }
390
-
391
- this._checkRequiredConfig();
392
-
393
- try {
394
- const shouldDeploy = process.argv.includes('--deploy');
395
-
396
- this.logger.info('▬▬▬▬▬▬▬▬▬▬▬[ Load Locales & Fonts ]▬▬▬▬▬▬▬▬▬▬▬');
397
- this.translator.loadLocales();
398
- if (this.helpers && this.helpers.fonts && typeof this.helpers.fonts.loadFonts === 'function') {
399
- this.helpers.fonts.loadFonts({ logger: this.logger });
400
- }
401
-
402
- this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Initialize Cache ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
403
-
404
- this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Kythia Addons ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
405
- this.addonManager = new AddonManager({ client: this.client, container: this.container });
406
- const allCommands = await this.addonManager.loadAddons(this);
407
-
408
- this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Load KythiaORM ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
409
- const sequelize = await KythiaORM({
410
- kythiaInstance: this,
411
- sequelize: this.sequelize,
412
- KythiaModel: this.dbDependencies.KythiaModel,
413
- logger: this.dbDependencies.logger,
414
- config: this.dbDependencies.config,
415
- });
416
-
417
- this.logger.info('🔄 Hydrating container with initialized models...');
418
- this.container.models = sequelize.models;
419
-
420
- const handlers = this.addonManager.getHandlers();
421
- this.eventManager = new EventManager({ client: this.client, container: this.container, eventHandlers: handlers.eventHandlers });
422
- this.eventManager.initialize();
423
-
424
- this.interactionManager = new InteractionManager({ client: this.client, container: this.container, handlers: handlers });
425
- this.interactionManager.initialize();
426
-
427
- this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬[ Deploy Commands ]▬▬▬▬▬▬▬▬▬▬▬▬▬');
428
- if (shouldDeploy) {
429
- await this._deployCommands(allCommands);
430
- } else {
431
- this.logger.info('⏭️ Skipping command deployment. Use --deploy flag to force update.');
432
- }
433
-
434
- this.shutdownManager = new ShutdownManager({ client: this.client, container: this.container });
435
- this.shutdownManager.initialize();
436
-
437
- this.logger.info('▬▬▬▬▬▬▬▬▬▬▬[ Systems Initializing ]▬▬▬▬▬▬▬▬▬▬▬▬');
438
-
439
- this.client.once('clientReady', async (c) => {
440
- this.logger.info(`🌸 Logged in as ${this.client.user.tag}`);
441
- this.logger.info(`🚀 Executing ${this.clientReadyHooks.length} client-ready hooks...`);
442
- for (const hook of this.clientReadyHooks) {
443
- try {
444
- await hook(c);
445
- } catch (error) {
446
- this.logger.error('Failed to execute a client-ready hook:', error);
447
- }
448
- }
449
- });
450
-
451
- await this.client.login(this.kythiaConfig.bot.token);
452
- } catch (error) {
453
- this.logger.error('❌ Kythia initialization failed:', error);
454
- if (this.kythiaConfig.sentry.dsn) {
455
- Sentry.captureException(error);
456
- }
457
- process.exit(1);
458
- }
459
- }
35
+ /**
36
+ * 🏗️ Kythia Constructor
37
+ * Initializes the Discord client, REST API, and dependency container.
38
+ * Sets up manager instances (but doesn't start them yet).
39
+ */
40
+ constructor({
41
+ config,
42
+ logger,
43
+ redis,
44
+ sequelize,
45
+ translator,
46
+ models,
47
+ helpers,
48
+ utils,
49
+ appRoot,
50
+ }) {
51
+ const missingDeps = [];
52
+ if (!config) missingDeps.push('config');
53
+ if (!logger) missingDeps.push('logger');
54
+ if (!translator) {
55
+ missingDeps.push('translator');
56
+ } else {
57
+ if (!translator.t) missingDeps.push('translator.t');
58
+ if (!translator.loadLocales) missingDeps.push('translator.loadLocales');
59
+ }
60
+ if (missingDeps.length > 0) {
61
+ console.error(
62
+ `FATAL: Missing required dependencies: ${missingDeps.join(', ')}.`,
63
+ );
64
+ process.exit(1);
65
+ }
66
+ this.kythiaConfig = config;
67
+ this.appRoot = appRoot || process.cwd();
68
+
69
+ this.client = KythiaClient();
70
+ this.client.commands = new Collection();
71
+ this.rest = new REST({ version: '10' }).setToken(
72
+ this.kythiaConfig.bot.token,
73
+ );
74
+
75
+ this.models = models;
76
+ this.helpers = helpers;
77
+ this.utils = utils;
78
+
79
+ this.redis = redis;
80
+ this.sequelize = sequelize;
81
+
82
+ this.logger = logger;
83
+ this.translator = translator;
84
+
85
+ this.container = {
86
+ client: this.client,
87
+ sequelize: this.sequelize,
88
+ logger: this.logger,
89
+ t: this.translator.t,
90
+ redis: this.redis,
91
+ kythiaConfig: this.kythiaConfig,
92
+ translator: this.translator,
93
+
94
+ models: this.models,
95
+ helpers: this.helpers,
96
+ appRoot: this.appRoot,
97
+ };
98
+
99
+ this.client.container = this.container;
100
+ this.client.cooldowns = new Collection();
101
+
102
+ this.dbReadyHooks = [];
103
+ this.clientReadyHooks = [];
104
+
105
+ this.addonManager = null;
106
+ this.interactionManager = null;
107
+ this.eventManager = null;
108
+ this.shutdownManager = null;
109
+ }
110
+
111
+ /**
112
+ * 🔍 Check Required Config
113
+ * Checks if all required configurations are set.
114
+ * Throws an error if any required config is missing.
115
+ */
116
+ _checkRequiredConfig() {
117
+ const requiredBotConfig = [
118
+ ['bot', 'token'],
119
+ ['bot', 'clientId'],
120
+ ['bot', 'clientSecret'],
121
+ ];
122
+ const missingBotConfigs = [];
123
+ for (const pathArr of requiredBotConfig) {
124
+ let value = this.kythiaConfig;
125
+ for (const key of pathArr) {
126
+ value = value?.[key];
127
+ }
128
+ if (value === undefined || value === null || value === '') {
129
+ missingBotConfigs.push(pathArr.join('.'));
130
+ }
131
+ }
132
+
133
+ if (!this.kythiaConfig.db) this.kythiaConfig.db = {};
134
+
135
+ let driver = this.kythiaConfig.db.driver;
136
+ if (!driver || driver === '') {
137
+ this.kythiaConfig.db.driver = 'sqlite';
138
+ driver = 'sqlite';
139
+ this.logger.info('💡 DB driver not specified. Defaulting to: sqlite');
140
+ } else {
141
+ driver = driver.toLowerCase();
142
+ this.kythiaConfig.db.driver = driver;
143
+ }
144
+
145
+ if (driver === 'sqlite') {
146
+ if (!this.kythiaConfig.db.name || this.kythiaConfig.db.name === '') {
147
+ this.kythiaConfig.db.name = 'kythiadata.sqlite';
148
+ }
149
+ }
150
+
151
+ const requiredDbConfig = [
152
+ ['db', 'driver'],
153
+ ['db', 'name'],
154
+ ];
155
+
156
+ if (driver !== 'sqlite') {
157
+ requiredDbConfig.push(
158
+ ['db', 'host'],
159
+ ['db', 'port'],
160
+ ['db', 'user'],
161
+ ['db', 'pass'],
162
+ );
163
+ }
164
+
165
+ const missingDbConfigs = [];
166
+ for (const pathArr of requiredDbConfig) {
167
+ let value = this.kythiaConfig;
168
+ for (const key of pathArr) {
169
+ value = value?.[key];
170
+ }
171
+ if (value === undefined || value === null || value === '') {
172
+ missingDbConfigs.push(pathArr.join('.'));
173
+ }
174
+ }
175
+
176
+ const missingConfigs = missingBotConfigs.concat(missingDbConfigs);
177
+
178
+ if (missingConfigs.length > 0) {
179
+ this.logger.error('❌ Required configurations are not set:');
180
+ for (const missing of missingConfigs) {
181
+ this.logger.error(` - ${missing}`);
182
+ }
183
+ process.exit(1);
184
+ }
185
+
186
+ this.logger.info('✔️ All required configurations are set');
187
+ }
188
+
189
+ /**
190
+ * 🔘 Register Button Handler
191
+ * Delegates to AddonManager
192
+ * @param {string} customId - The customId of the button
193
+ * @param {Function} handler - The handler function to execute
194
+ */
195
+ registerButtonHandler(customId, handler) {
196
+ if (this.addonManager) {
197
+ this.addonManager.registerButtonHandler(customId, handler);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * 📝 Register Modal Handler
203
+ * Delegates to AddonManager
204
+ * @param {string} customIdPrefix - The prefix of the modal customId
205
+ * @param {Function} handler - The handler function to execute
206
+ */
207
+ registerModalHandler(customIdPrefix, handler) {
208
+ if (this.addonManager) {
209
+ this.addonManager.registerModalHandler(customIdPrefix, handler);
210
+ }
211
+ }
212
+ /**
213
+ * 🟦 Register Select Menu Handler
214
+ * Delegates to AddonManager
215
+ * @param {string} customIdPrefix - The prefix of the select menu customId
216
+ * @param {Function} handler - The handler function to execute
217
+ */
218
+ registerSelectMenuHandler(customIdPrefix, handler) {
219
+ if (this.addonManager) {
220
+ this.addonManager.registerSelectMenuHandler(customIdPrefix, handler);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * 🛡️ Validate License (Stub)
226
+ * Placeholder for license validation logic for addons.
227
+ * @param {string} licenseKey - The license key to validate
228
+ * @param {string} addonName - The name of the addon
229
+ * @returns {Promise<boolean>} Always returns true (stub)
230
+ */
231
+ async _validateLicense() {
232
+ return true;
233
+ }
234
+
235
+ /**
236
+ * 🚀 Deploy Commands to Discord
237
+ * Deploys all registered slash commands to Discord using the REST API.
238
+ * @param {Array} commands - Array of command data to deploy
239
+ */
240
+ async _deployCommands(commands) {
241
+ if (!commands || commands.length === 0) {
242
+ this.logger.info('No commands to deploy.');
243
+ return;
244
+ }
245
+ try {
246
+ const { slash, user, message } = this._getCommandCounts(commands);
247
+ const clientId = this.kythiaConfig.bot.clientId;
248
+ const devGuildId = this.kythiaConfig.bot.devGuildId;
249
+
250
+ if (
251
+ this.kythiaConfig.env === 'dev' ||
252
+ this.kythiaConfig.env === 'development'
253
+ ) {
254
+ if (!devGuildId) {
255
+ this.logger.warn(
256
+ '⚠️ devGuildId not set in config. Skipping guild command deployment.',
257
+ );
258
+ return;
259
+ }
260
+ this.logger.info(`🟠 Deploying to GUILD ${devGuildId}...`);
261
+ await this.rest.put(
262
+ Routes.applicationGuildCommands(clientId, devGuildId),
263
+ { body: commands },
264
+ );
265
+ this.logger.info('✅ Guild commands deployed instantly!');
266
+ } else {
267
+ this.logger.info(`🟢 Deploying globally...`);
268
+ await this.rest.put(Routes.applicationCommands(clientId), {
269
+ body: commands,
270
+ });
271
+ this.logger.info('✅ Global commands deployed successfully!');
272
+ if (devGuildId) {
273
+ this.logger.info(
274
+ `🧹 Clearing old commands from dev guild: ${devGuildId}...`,
275
+ );
276
+ try {
277
+ await this.rest.put(
278
+ Routes.applicationGuildCommands(clientId, devGuildId),
279
+ { body: [] },
280
+ );
281
+ this.logger.info('✅ Dev guild commands cleared successfully.');
282
+ } catch (err) {
283
+ this.logger.warn(
284
+ `⚠️ Could not clear dev guild commands (maybe it was already clean): ${err.message}`,
285
+ );
286
+ }
287
+ }
288
+ }
289
+
290
+ this.logger.info(`⭕ All Slash Commands: ${commands.length}`);
291
+ this.logger.info(`⭕ Top Level Slash Commands: ${slash}`);
292
+ this.logger.info(`⭕ User Context Menu: ${user}`);
293
+ this.logger.info(`⭕ Message Context Menu: ${message}`);
294
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
295
+ } catch (err) {
296
+ this.logger.error('❌ Failed to deploy slash commands:', err);
297
+ }
298
+ }
299
+
300
+ /**
301
+ * 🧮 Count command types from JSON array
302
+ * @param {Array} commandJsonArray - Array command data to be deployed
303
+ * @returns {object} - Object containing counts { slash, user, message }
304
+ * @private
305
+ */
306
+ _getCommandCounts(commandJsonArray) {
307
+ const counts = { slash: 0, user: 0, message: 0 };
308
+
309
+ if (!Array.isArray(commandJsonArray)) {
310
+ this.logger.warn(
311
+ 'commandJsonArray is not iterable. Returning zero counts.',
312
+ );
313
+ return counts;
314
+ }
315
+
316
+ for (const cmd of commandJsonArray) {
317
+ switch (cmd?.type) {
318
+ case 1:
319
+ case undefined:
320
+ counts.slash++;
321
+ break;
322
+ case 2:
323
+ counts.user++;
324
+ break;
325
+ case 3:
326
+ counts.message++;
327
+ break;
328
+ }
329
+ }
330
+ return counts;
331
+ }
332
+
333
+ /**
334
+ * Adds a callback to be executed when the database is ready.
335
+ * The callback will be executed after all database models have been synchronized.
336
+ * @param {function} callback - Callback to be executed when the database is ready
337
+ */
338
+ addDbReadyHook(callback) {
339
+ this.dbReadyHooks.push(callback);
340
+ }
341
+
342
+ /**
343
+ * Adds a callback to be executed when the client is ready.
344
+ * @param {function} callback - Callback to be executed when the client is ready
345
+ */
346
+ addClientReadyHook(callback) {
347
+ this.clientReadyHooks.push(callback);
348
+ }
349
+
350
+ /**
351
+ * 🌸 Start the Kythia Bot
352
+ * Main orchestration method that:
353
+ * 1. Initializes Redis cache
354
+ * 2. Creates and starts all managers
355
+ * 3. Loads addons via AddonManager
356
+ * 4. Initializes database
357
+ * 5. Sets up interaction and event handlers
358
+ * 6. Deploys commands
359
+ * 7. Logs in to Discord
360
+ */
361
+ async start() {
362
+ const version = require('../package.json').version;
363
+ const clc = require('cli-color');
364
+ const figletText = (text, opts) =>
365
+ new Promise((resolve, reject) => {
366
+ figlet.text(text, opts, (err, data) => {
367
+ if (err) reject(err);
368
+ else resolve(data);
369
+ });
370
+ });
371
+
372
+ try {
373
+ const data = await figletText('KYTHIA', {
374
+ font: 'ANSI Shadow',
375
+ horizontalLayout: 'full',
376
+ verticalLayout: 'full',
377
+ });
378
+
379
+ const infoLines = [
380
+ clc.cyan('Created by kenndeclouv'),
381
+ clc.cyan('Discord Support: ') + clc.underline('https://dsc.gg/kythia'),
382
+ clc.cyan('Official Documentation: ') +
383
+ clc.underline('https://kythia.my.id/commands'),
384
+ '',
385
+ clc.cyanBright(`Kythia version: ${version}`),
386
+ '',
387
+ clc.yellowBright('Respect my work by not removing the credit'),
388
+ ];
389
+
390
+ const rawInfoLines = infoLines.map((line) => clc.strip(line));
391
+ const infoMaxLen = Math.max(...rawInfoLines.map((l) => l.length));
392
+ const pad = 8;
393
+ const borderWidth = infoMaxLen + pad * 2;
394
+ const borderChar = clc.cyanBright('═');
395
+ const sideChar = clc.cyanBright('║');
396
+ const topBorder = clc.cyanBright(`╔${borderChar.repeat(borderWidth)}╗`);
397
+ const bottomBorder = clc.cyanBright(
398
+ `╚${borderChar.repeat(borderWidth)}╝`,
399
+ );
400
+ const emptyLine = sideChar + ' '.repeat(borderWidth) + sideChar;
401
+
402
+ const figletLines = data.split('\n');
403
+ const centeredFigletInBorder = figletLines
404
+ .map((line) => {
405
+ const rawLen = clc.strip(line).length;
406
+ const spaces = ' '.repeat(
407
+ Math.max(0, Math.floor((borderWidth - rawLen) / 2)),
408
+ );
409
+ return (
410
+ sideChar +
411
+ spaces +
412
+ clc.cyanBright(line) +
413
+ ' '.repeat(borderWidth - spaces.length - rawLen) +
414
+ sideChar
415
+ );
416
+ })
417
+ .join('\n');
418
+
419
+ const centeredInfo = infoLines
420
+ .map((line, idx) => {
421
+ const raw = rawInfoLines[idx];
422
+ const spaces = ' '.repeat(Math.floor((borderWidth - raw.length) / 2));
423
+ return (
424
+ sideChar +
425
+ spaces +
426
+ line +
427
+ ' '.repeat(borderWidth - spaces.length - raw.length) +
428
+ sideChar
429
+ );
430
+ })
431
+ .join('\n');
432
+
433
+ console.log(`\n${topBorder}`);
434
+ console.log(emptyLine);
435
+ console.log(centeredFigletInBorder);
436
+ console.log(emptyLine);
437
+ console.log(centeredInfo);
438
+ console.log(emptyLine);
439
+ console.log(`${bottomBorder}\n`);
440
+ } catch (err) {
441
+ this.logger.error('❌ Failed to render figlet banner:', err);
442
+ }
443
+
444
+ this.logger.info('🚀 Starting kythia...');
445
+
446
+ if (this.kythiaConfig.sentry.dsn) {
447
+ Sentry.init({
448
+ dsn: this.kythiaConfig.sentry.dsn,
449
+ tracesSampleRate: 1.0,
450
+ profilesSampleRate: 1.0,
451
+ });
452
+ this.logger.info('✔️ Sentry Error Tracking is ACTIVE');
453
+ } else {
454
+ this.logger.warn(
455
+ '🟠 Sentry DSN not found in config. Error tracking is INACTIVE.',
456
+ );
457
+ }
458
+
459
+ this._checkRequiredConfig();
460
+
461
+ try {
462
+ const shouldDeploy = process.argv.includes('--deploy');
463
+
464
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬[ Load Locales & Fonts ]▬▬▬▬▬▬▬▬▬▬▬');
465
+ this.translator.loadLocales();
466
+ if (
467
+ this.helpers?.fonts &&
468
+ typeof this.helpers.fonts.loadFonts === 'function'
469
+ ) {
470
+ this.helpers.fonts.loadFonts({ logger: this.logger });
471
+ }
472
+
473
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Initialize Cache ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
474
+
475
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Kythia Addons ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
476
+ this.addonManager = new AddonManager({
477
+ client: this.client,
478
+ container: this.container,
479
+ });
480
+ const allCommands = await this.addonManager.loadAddons(this);
481
+
482
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Connect Database ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
483
+ await this.sequelize.authenticate();
484
+
485
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Run Migrations ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
486
+ await KythiaMigrator({
487
+ sequelize: this.sequelize,
488
+ container: this.container,
489
+ logger: this.logger,
490
+ });
491
+
492
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Boot Models ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
493
+ await ModelLoader(this, this.sequelize);
494
+
495
+ this.logger.info('🔄 Hydrating container with initialized models...');
496
+
497
+ const handlers = this.addonManager.getHandlers();
498
+ this.eventManager = new EventManager({
499
+ client: this.client,
500
+ container: this.container,
501
+ eventHandlers: handlers.eventHandlers,
502
+ });
503
+ this.eventManager.initialize();
504
+
505
+ this.interactionManager = new InteractionManager({
506
+ client: this.client,
507
+ container: this.container,
508
+ handlers: handlers,
509
+ });
510
+ this.interactionManager.initialize();
511
+
512
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬[ Deploy Commands ]▬▬▬▬▬▬▬▬▬▬▬▬▬');
513
+ if (shouldDeploy) {
514
+ await this._deployCommands(allCommands);
515
+ } else {
516
+ this.logger.info(
517
+ '⏭️ Skipping command deployment. Use --deploy flag to force update.',
518
+ );
519
+ }
520
+
521
+ this.shutdownManager = new ShutdownManager({
522
+ client: this.client,
523
+ container: this.container,
524
+ });
525
+ this.shutdownManager.initialize();
526
+
527
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬[ Systems Initializing ]▬▬▬▬▬▬▬▬▬▬▬▬');
528
+
529
+ this.client.once('clientReady', async (c) => {
530
+ this.logger.info(`🌸 Logged in as ${this.client.user.tag}`);
531
+ this.logger.info(
532
+ `🚀 Executing ${this.clientReadyHooks.length} client-ready hooks...`,
533
+ );
534
+ for (const hook of this.clientReadyHooks) {
535
+ try {
536
+ await hook(c);
537
+ } catch (error) {
538
+ this.logger.error('Failed to execute a client-ready hook:', error);
539
+ }
540
+ }
541
+ });
542
+
543
+ await this.client.login(this.kythiaConfig.bot.token);
544
+ } catch (error) {
545
+ this.logger.error('❌ Kythia initialization failed:', error);
546
+ if (this.kythiaConfig.sentry.dsn) {
547
+ Sentry.captureException(error);
548
+ }
549
+ process.exit(1);
550
+ }
551
+ }
460
552
  }
461
553
 
462
554
  module.exports = Kythia;