kythia-core 0.9.3-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 ADDED
@@ -0,0 +1,430 @@
1
+ /**
2
+ * 🤖 Main Kythia Entrypoint
3
+ *
4
+ * @file src/Kythia.js
5
+ * @copyright © 2025 kenndeclouv
6
+ * @assistant chaa & graa
7
+ * @version 0.9.3-beta
8
+ *
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.
12
+ *
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
18
+ */
19
+
20
+ const { REST, Routes, Collection } = require('discord.js');
21
+ const KythiaORM = require('./database/KythiaORM');
22
+ const KythiaClient = require('./KythiaClient');
23
+ const Sentry = require('@sentry/node');
24
+ const figlet = require('figlet');
25
+
26
+ const InteractionManager = require('./managers/InteractionManager');
27
+ const ShutdownManager = require('./managers/ShutdownManager');
28
+ const AddonManager = require('./managers/AddonManager');
29
+ const EventManager = require('./managers/EventManager');
30
+
31
+ 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 requiredConfig = [
101
+ ['bot', 'token'],
102
+ ['bot', 'clientId'],
103
+ ['bot', 'clientSecret'],
104
+ ['db', 'driver'],
105
+ ['db', 'host'],
106
+ ['db', 'port'],
107
+ ['db', 'name'],
108
+ ['db', 'user'],
109
+ ];
110
+
111
+ const missingConfigs = [];
112
+
113
+ for (const pathArr of requiredConfig) {
114
+ let value = this.kythiaConfig;
115
+ for (const key of pathArr) {
116
+ value = value?.[key];
117
+ }
118
+
119
+ if (value === undefined || value === null || value === '') {
120
+ missingConfigs.push(pathArr.join('.'));
121
+ }
122
+ }
123
+
124
+ if (missingConfigs.length > 0) {
125
+ this.logger.error('❌ Required configurations are not set:');
126
+ for (const missing of missingConfigs) {
127
+ this.logger.error(` - ${missing}`);
128
+ }
129
+ process.exit(1);
130
+ }
131
+
132
+ this.logger.info('✔️ All required configurations are set');
133
+ }
134
+
135
+ /**
136
+ * 🔘 Register Button Handler
137
+ * Delegates to AddonManager
138
+ * @param {string} customId - The customId of the button
139
+ * @param {Function} handler - The handler function to execute
140
+ */
141
+ registerButtonHandler(customId, handler) {
142
+ if (this.addonManager) {
143
+ this.addonManager.registerButtonHandler(customId, handler);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 📝 Register Modal Handler
149
+ * Delegates to AddonManager
150
+ * @param {string} customIdPrefix - The prefix of the modal customId
151
+ * @param {Function} handler - The handler function to execute
152
+ */
153
+ registerModalHandler(customIdPrefix, handler) {
154
+ if (this.addonManager) {
155
+ this.addonManager.registerModalHandler(customIdPrefix, handler);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * 🛡️ Validate License (Stub)
161
+ * Placeholder for license validation logic for addons.
162
+ * @param {string} licenseKey - The license key to validate
163
+ * @param {string} addonName - The name of the addon
164
+ * @returns {Promise<boolean>} Always returns true (stub)
165
+ */
166
+ async _validateLicense(licenseKey, addonName) {
167
+ return true;
168
+ }
169
+
170
+ /**
171
+ * 🚀 Deploy Commands to Discord
172
+ * Deploys all registered slash commands to Discord using the REST API.
173
+ * @param {Array} commands - Array of command data to deploy
174
+ */
175
+ async _deployCommands(commands) {
176
+ if (!commands || commands.length === 0) {
177
+ this.logger.info('No commands to deploy.');
178
+ return;
179
+ }
180
+ try {
181
+ const { slash, user, message } = this._getCommandCounts(commands);
182
+ const clientId = this.kythiaConfig.bot.clientId;
183
+ const devGuildId = this.kythiaConfig.bot.devGuildId;
184
+
185
+ let deployType = '';
186
+ if (this.kythiaConfig.env == 'dev' || this.kythiaConfig.env == 'development') {
187
+ if (!devGuildId) {
188
+ this.logger.warn('⚠️ devGuildId not set in config. Skipping guild command deployment.');
189
+ return;
190
+ }
191
+ this.logger.info(`🟠 Deploying to GUILD ${devGuildId}...`);
192
+ await this.rest.put(Routes.applicationGuildCommands(clientId, devGuildId), { body: commands });
193
+ this.logger.info('✅ Guild commands deployed instantly!');
194
+ deployType = `Guild (${devGuildId})`;
195
+ } else {
196
+ this.logger.info(`🟢 Deploying globally...`);
197
+ await this.rest.put(Routes.applicationCommands(clientId), { body: commands });
198
+ this.logger.info('✅ Global commands deployed successfully!');
199
+ if (devGuildId) {
200
+ this.logger.info(`🧹 Clearing old commands from dev guild: ${devGuildId}...`);
201
+ try {
202
+ await this.rest.put(Routes.applicationGuildCommands(clientId, devGuildId), { body: [] });
203
+ this.logger.info('✅ Dev guild commands cleared successfully.');
204
+ } catch (err) {
205
+ this.logger.warn(`⚠️ Could not clear dev guild commands (maybe it was already clean): ${err.message}`);
206
+ }
207
+ }
208
+ deployType = 'Global';
209
+ }
210
+
211
+ this.logger.info(`⭕ All Slash Commands: ${commands.length}`);
212
+ this.logger.info(`⭕ Top Level Slash Commands: ${slash}`);
213
+ this.logger.info(`⭕ User Context Menu: ${user}`);
214
+ this.logger.info(`⭕ Message Context Menu: ${message}`);
215
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
216
+ } catch (err) {
217
+ this.logger.error('❌ Failed to deploy slash commands:', err);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 🧮 Count command types from JSON array
223
+ * @param {Array} commandJsonArray - Array command data to be deployed
224
+ * @returns {object} - Object containing counts { slash, user, message }
225
+ * @private
226
+ */
227
+ _getCommandCounts(commandJsonArray) {
228
+ const counts = { slash: 0, user: 0, message: 0 };
229
+
230
+ if (!Array.isArray(commandJsonArray)) {
231
+ this.logger.warn('commandJsonArray is not iterable. Returning zero counts.');
232
+ return counts;
233
+ }
234
+
235
+ for (const cmd of commandJsonArray) {
236
+ switch (cmd?.type) {
237
+ case 1:
238
+ case undefined:
239
+ counts.slash++;
240
+ break;
241
+ case 2:
242
+ counts.user++;
243
+ break;
244
+ case 3:
245
+ counts.message++;
246
+ break;
247
+ }
248
+ }
249
+ return counts;
250
+ }
251
+
252
+ /**
253
+ * Adds a callback to be executed when the database is ready.
254
+ * The callback will be executed after all database models have been synchronized.
255
+ * @param {function} callback - Callback to be executed when the database is ready
256
+ */
257
+ addDbReadyHook(callback) {
258
+ this.dbReadyHooks.push(callback);
259
+ }
260
+
261
+ /**
262
+ * Adds a callback to be executed when the client is ready.
263
+ * @param {function} callback - Callback to be executed when the client is ready
264
+ */
265
+ addClientReadyHook(callback) {
266
+ this.clientReadyHooks.push(callback);
267
+ }
268
+
269
+ /**
270
+ * 🌸 Start the Kythia Bot
271
+ * Main orchestration method that:
272
+ * 1. Initializes Redis cache
273
+ * 2. Creates and starts all managers
274
+ * 3. Loads addons via AddonManager
275
+ * 4. Initializes database
276
+ * 5. Sets up interaction and event handlers
277
+ * 6. Deploys commands
278
+ * 7. Logs in to Discord
279
+ */
280
+ async start() {
281
+ const version = require('../package.json').version;
282
+ const clc = require('cli-color');
283
+ const figletText = (text, opts) =>
284
+ new Promise((resolve, reject) => {
285
+ figlet.text(text, opts, (err, data) => {
286
+ if (err) reject(err);
287
+ else resolve(data);
288
+ });
289
+ });
290
+
291
+ try {
292
+ const data = await figletText('KYTHIA', {
293
+ font: 'ANSI Shadow',
294
+ horizontalLayout: 'full',
295
+ verticalLayout: 'full',
296
+ });
297
+
298
+ const infoLines = [
299
+ clc.cyan('Created by kenndeclouv'),
300
+ clc.cyan('Discord Support: ') + clc.underline('https://dsc.gg/kythia'),
301
+ clc.cyan('Official Documentation: ') + clc.underline('https://kythia.my.id/commands'),
302
+ '',
303
+ clc.cyanBright(`Kythia version: ${version}`),
304
+ '',
305
+ clc.yellowBright('Respect my work by not removing the credit'),
306
+ ];
307
+
308
+ const rawInfoLines = infoLines.map((line) => clc.strip(line));
309
+ const infoMaxLen = Math.max(...rawInfoLines.map((l) => l.length));
310
+ const pad = 8;
311
+ const borderWidth = infoMaxLen + pad * 2;
312
+ const borderChar = clc.cyanBright('═');
313
+ const sideChar = clc.cyanBright('║');
314
+ const topBorder = clc.cyanBright('╔' + borderChar.repeat(borderWidth) + '╗');
315
+ const bottomBorder = clc.cyanBright('╚' + borderChar.repeat(borderWidth) + '╝');
316
+ const emptyLine = sideChar + ' '.repeat(borderWidth) + sideChar;
317
+
318
+ const figletLines = data.split('\n');
319
+ const centeredFigletInBorder = figletLines
320
+ .map((line) => {
321
+ const rawLen = clc.strip(line).length;
322
+ const spaces = ' '.repeat(Math.max(0, Math.floor((borderWidth - rawLen) / 2)));
323
+ return sideChar + spaces + clc.cyanBright(line) + ' '.repeat(borderWidth - spaces.length - rawLen) + sideChar;
324
+ })
325
+ .join('\n');
326
+
327
+ const centeredInfo = infoLines
328
+ .map((line, idx) => {
329
+ const raw = rawInfoLines[idx];
330
+ const spaces = ' '.repeat(Math.floor((borderWidth - raw.length) / 2));
331
+ return sideChar + spaces + line + ' '.repeat(borderWidth - spaces.length - raw.length) + sideChar;
332
+ })
333
+ .join('\n');
334
+
335
+ console.log('\n' + topBorder);
336
+ console.log(emptyLine);
337
+ console.log(centeredFigletInBorder);
338
+ console.log(emptyLine);
339
+ console.log(centeredInfo);
340
+ console.log(emptyLine);
341
+ console.log(bottomBorder + '\n');
342
+ } catch (err) {
343
+ this.logger.error('❌ Failed to render figlet banner:', err);
344
+ }
345
+
346
+ this.logger.info('🚀 Starting kythia...');
347
+
348
+ if (this.kythiaConfig.sentry.dsn) {
349
+ Sentry.init({
350
+ dsn: this.kythiaConfig.sentry.dsn,
351
+ tracesSampleRate: 1.0,
352
+ profilesSampleRate: 1.0,
353
+ });
354
+ this.logger.info('✔️ Sentry Error Tracking is ACTIVE');
355
+ } else {
356
+ this.logger.warn('🟠 Sentry DSN not found in config. Error tracking is INACTIVE.');
357
+ }
358
+
359
+ this._checkRequiredConfig();
360
+
361
+ try {
362
+ const shouldDeploy = process.argv.includes('--deploy');
363
+
364
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬[ Load Locales & Fonts ]▬▬▬▬▬▬▬▬▬▬▬');
365
+ this.translator.loadLocales();
366
+ if (this.helpers && this.helpers.fonts && typeof this.helpers.fonts.loadFonts === 'function') {
367
+ this.helpers.fonts.loadFonts({ logger: this.logger });
368
+ }
369
+
370
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Initialize Cache ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
371
+
372
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Kythia Addons ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
373
+ this.addonManager = new AddonManager({ client: this.client, container: this.container });
374
+ const allCommands = await this.addonManager.loadAddons(this);
375
+
376
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬▬[ Load KythiaORM ]▬▬▬▬▬▬▬▬▬▬▬▬▬▬');
377
+ const sequelize = await KythiaORM({
378
+ kythiaInstance: this,
379
+ sequelize: this.sequelize,
380
+ KythiaModel: this.dbDependencies.KythiaModel,
381
+ logger: this.dbDependencies.logger,
382
+ config: this.dbDependencies.config,
383
+ });
384
+
385
+ this.logger.info('🔄 Hydrating container with initialized models...');
386
+ this.container.models = sequelize.models;
387
+
388
+ const handlers = this.addonManager.getHandlers();
389
+ this.eventManager = new EventManager({ client: this.client, container: this.container, eventHandlers: handlers.eventHandlers });
390
+ this.eventManager.initialize();
391
+
392
+ this.interactionManager = new InteractionManager({ client: this.client, container: this.container, handlers: handlers });
393
+ this.interactionManager.initialize();
394
+
395
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬[ Deploy Commands ]▬▬▬▬▬▬▬▬▬▬▬▬▬');
396
+ if (shouldDeploy) {
397
+ await this._deployCommands(allCommands);
398
+ } else {
399
+ this.logger.info('⏭️ Skipping command deployment. Use --deploy flag to force update.');
400
+ }
401
+
402
+ this.shutdownManager = new ShutdownManager({ client: this.client, container: this.container });
403
+ this.shutdownManager.initialize();
404
+
405
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬[ Systems Initializing ]▬▬▬▬▬▬▬▬▬▬▬▬');
406
+
407
+ this.client.once('clientReady', async (c) => {
408
+ this.logger.info(`🌸 Logged in as ${this.client.user.tag}`);
409
+ this.logger.info(`🚀 Executing ${this.clientReadyHooks.length} client-ready hooks...`);
410
+ for (const hook of this.clientReadyHooks) {
411
+ try {
412
+ await hook(c);
413
+ } catch (error) {
414
+ this.logger.error('Failed to execute a client-ready hook:', error);
415
+ }
416
+ }
417
+ });
418
+
419
+ await this.client.login(this.kythiaConfig.bot.token);
420
+ } catch (error) {
421
+ this.logger.error('❌ Kythia initialization failed:', error);
422
+ if (this.kythiaConfig.sentry.dsn) {
423
+ Sentry.captureException(error);
424
+ }
425
+ process.exit(1);
426
+ }
427
+ }
428
+ }
429
+
430
+ module.exports = Kythia;
@@ -0,0 +1,53 @@
1
+ const { Client, GatewayIntentBits, Partials, Options } = require('discord.js');
2
+
3
+ module.exports = function kythiaClient() {
4
+ const client = new Client({
5
+ intents: [
6
+ GatewayIntentBits.Guilds,
7
+ GatewayIntentBits.GuildMessages,
8
+ GatewayIntentBits.MessageContent,
9
+ GatewayIntentBits.GuildMembers,
10
+ GatewayIntentBits.GuildModeration,
11
+ GatewayIntentBits.GuildInvites,
12
+ GatewayIntentBits.GuildVoiceStates,
13
+ GatewayIntentBits.AutoModerationExecution,
14
+ GatewayIntentBits.DirectMessages,
15
+ GatewayIntentBits.DirectMessageReactions,
16
+ GatewayIntentBits.DirectMessageTyping,
17
+ GatewayIntentBits.GuildExpressions,
18
+ ],
19
+
20
+ partials: [Partials.Message, Partials.Channel, Partials.Reaction, Partials.User, Partials.GuildMember],
21
+
22
+ makeCache: Options.cacheWithLimits({
23
+ MessageManager: 25,
24
+ PresenceManager: 0,
25
+ GuildMemberManager: {
26
+ max: 100,
27
+ keepOverLimit: (member) =>
28
+ (client.user && member.id === client.user.id) ||
29
+ (member.guild && member.id === member.guild.ownerId) ||
30
+ (member.voice && member.voice.channelId !== null),
31
+ },
32
+ ThreadManager: 10,
33
+ }),
34
+
35
+ sweepers: {
36
+ ...Options.DefaultSweeperSettings,
37
+ messages: {
38
+ interval: 3600,
39
+ lifetime: 1800,
40
+ },
41
+
42
+ threads: {
43
+ interval: 3600,
44
+ lifetime: 1800,
45
+ },
46
+ users: {
47
+ interval: 3600,
48
+ filter: () => (user) => user && !user.bot,
49
+ },
50
+ },
51
+ });
52
+ return client;
53
+ };