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