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.
@@ -0,0 +1,197 @@
1
+ /**
2
+ * ๐Ÿ›‘ Shutdown Manager
3
+ *
4
+ * @file src/managers/ShutdownManager.js
5
+ * @copyright ยฉ 2025 kenndeclouv
6
+ * @assistant chaa & graa
7
+ * @version 0.9.3-beta
8
+ *
9
+ * @description
10
+ * Handles graceful shutdown procedures including interval tracking,
11
+ * component cleanup, and resource management.
12
+ */
13
+
14
+ const exitHook = require('async-exit-hook');
15
+
16
+ class ShutdownManager {
17
+ /**
18
+ * ๐Ÿ—๏ธ ShutdownManager Constructor
19
+ * @param {Object} client - Discord client instance
20
+ * @param {Object} container - Dependency container
21
+ */
22
+ constructor({ client, container }) {
23
+ this.client = client;
24
+ this.container = container;
25
+ this._activeIntervals = new Set();
26
+ this._messagesWithActiveCollectors = new Set();
27
+ this._collectorPatched = false;
28
+ this._cleanupAttached = false;
29
+
30
+ this.logger = this.container.logger;
31
+ }
32
+
33
+ /**
34
+ * ๐Ÿ•ต๏ธโ€โ™‚๏ธ [GLOBAL PATCH] Overrides global interval functions to track all active intervals.
35
+ * This allows for a truly generic and scalable graceful shutdown of all timed tasks.
36
+ */
37
+ initializeGlobalIntervalTracker() {
38
+ if (!this._activeIntervals) this._activeIntervals = new Set();
39
+
40
+ const botInstance = this;
41
+ const originalSetInterval = global.setInterval;
42
+ const originalClearInterval = global.clearInterval;
43
+
44
+ global.setInterval = function (...args) {
45
+ const intervalId = originalSetInterval.apply(this, args);
46
+
47
+ botInstance._activeIntervals.add(intervalId);
48
+ return intervalId;
49
+ };
50
+
51
+ global.clearInterval = function (intervalId) {
52
+ originalClearInterval.apply(this, [intervalId]);
53
+
54
+ botInstance._activeIntervals.delete(intervalId);
55
+ };
56
+
57
+ this.logger.info('โœ… Global setInterval/clearInterval has been patched for tracking.');
58
+ }
59
+
60
+ /**
61
+ * ๐Ÿ›‘ [FINAL ARCHITECTURE v5] Manages ALL graceful shutdown procedures.
62
+ * This version patches the core message sending/editing methods to automatically
63
+ * track ANY message with components, regardless of how its interactions are handled.
64
+ */
65
+ initializeShutdownCollectors() {
66
+ if (!this._messagesWithActiveCollectors) this._messagesWithActiveCollectors = new Set();
67
+
68
+ if (!this._collectorPatched) {
69
+ const origCreateCollector = require('discord.js').Message.prototype.createMessageComponentCollector;
70
+ const botInstance = this;
71
+
72
+ require('discord.js').Message.prototype.createMessageComponentCollector = function (...args) {
73
+ const collector = origCreateCollector.apply(this, args);
74
+ const message = this;
75
+
76
+ if (botInstance._messagesWithActiveCollectors) {
77
+ botInstance._messagesWithActiveCollectors.add(message);
78
+ }
79
+
80
+ collector.once('end', () => {
81
+ if (botInstance._messagesWithActiveCollectors) {
82
+ botInstance._messagesWithActiveCollectors.delete(message);
83
+ }
84
+ });
85
+
86
+ return collector;
87
+ };
88
+ this._collectorPatched = true;
89
+ this.logger.info('โœ… Corrected collector-based component tracking has been patched.');
90
+ }
91
+
92
+ if (!this._cleanupAttached) {
93
+ const cleanupAndFlush = async (callback) => {
94
+ this.logger.info('๐Ÿ›‘ Graceful shutdown initiated...');
95
+
96
+ if (this._activeIntervals && this._activeIntervals.size > 0) {
97
+ this.logger.info(`๐Ÿ›‘ Halting ${this._activeIntervals.size} active global intervals...`);
98
+ for (const intervalId of this._activeIntervals) {
99
+ clearInterval(intervalId);
100
+ }
101
+ }
102
+
103
+ const messagesToProcess = this._messagesWithActiveCollectors;
104
+
105
+ if (messagesToProcess && messagesToProcess.size > 0) {
106
+ this.logger.info(`๐Ÿ›‘ Disabling components on up to ${messagesToProcess.size} messages.`);
107
+ const editPromises = [];
108
+
109
+ function disableRecursively(components) {
110
+ return components.map((comp) => {
111
+ if (comp.components && Array.isArray(comp.components)) {
112
+ comp.components = disableRecursively(comp.components);
113
+ }
114
+
115
+ if (comp.type === 2 || comp.type === 3 || comp.type >= 5) {
116
+ return { ...comp, disabled: true };
117
+ }
118
+ return comp;
119
+ });
120
+ }
121
+
122
+ for (const msg of messagesToProcess) {
123
+ if (!msg.editable || !msg.components || msg.components.length === 0) continue;
124
+ try {
125
+ const rawComponents = msg.components.map((c) => c.toJSON());
126
+ const disabledComponents = disableRecursively(rawComponents);
127
+ editPromises.push(msg.edit({ components: disabledComponents }).catch(() => {}));
128
+ } catch (e) {}
129
+ }
130
+ await Promise.allSettled(editPromises);
131
+ }
132
+ this.logger.info('โœ… Component cleanup complete.');
133
+
134
+ this.logger.info('๐Ÿšฐ Flushing remaining logs...');
135
+ this.logger.on('finish', () => {
136
+ console.log('โณ Logger has flushed. Kythia is now safely shutting down.');
137
+ if (callback) callback();
138
+ });
139
+ this.logger.end();
140
+ setTimeout(() => {
141
+ console.log('โณ Logger flush timeout. Forcing exit.');
142
+ if (callback) callback();
143
+ }, 4000);
144
+ };
145
+
146
+ exitHook(cleanupAndFlush);
147
+ process.on('unhandledRejection', (error) => {
148
+ this.logger.error('โ€ผ๏ธ UNHANDLED PROMISE REJECTION:', error);
149
+ });
150
+ process.on('uncaughtException', (error) => {
151
+ this.logger.error('โ€ผ๏ธ UNCAUGHT EXCEPTION! Bot will shutdown.', error);
152
+ process.exit(1);
153
+ });
154
+
155
+ this._cleanupAttached = true;
156
+ this.logger.info('๐Ÿ›ก๏ธ Graceful shutdown and error handlers are now active.');
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Initialize all shutdown procedures
162
+ */
163
+ initialize() {
164
+ this.initializeGlobalIntervalTracker();
165
+ this.initializeShutdownCollectors();
166
+ }
167
+
168
+ /**
169
+ * Get active intervals count
170
+ * @returns {number} Number of active intervals
171
+ */
172
+ getActiveIntervalsCount() {
173
+ return this._activeIntervals ? this._activeIntervals.size : 0;
174
+ }
175
+
176
+ /**
177
+ * Get messages with active collectors count
178
+ * @returns {number} Number of messages with active collectors
179
+ */
180
+ getActiveCollectorsCount() {
181
+ return this._messagesWithActiveCollectors ? this._messagesWithActiveCollectors.size : 0;
182
+ }
183
+
184
+ /**
185
+ * Force cleanup (for testing purposes)
186
+ */
187
+ forceCleanup() {
188
+ if (this._activeIntervals) {
189
+ for (const intervalId of this._activeIntervals) {
190
+ clearInterval(intervalId);
191
+ }
192
+ this._activeIntervals.clear();
193
+ }
194
+ }
195
+ }
196
+
197
+ module.exports = ShutdownManager;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Base Command Structure
3
+ * @class BaseCommand
4
+ * @description Base class for all command types (slash commands, context menus, etc.)
5
+ */
6
+ class BaseCommand {
7
+ /**
8
+ * @param {Object} container - Dependency injection container
9
+ */
10
+ constructor(container) {
11
+ if (!container) {
12
+ throw new Error('Container is required for BaseCommand');
13
+ }
14
+
15
+ this.container = container;
16
+ this.client = container.client;
17
+ this.logger = container.logger;
18
+ this.t = container.t;
19
+ this.models = container.models;
20
+ this.kythiaConfig = container.kythiaConfig;
21
+ this.helpers = container.helpers;
22
+
23
+ this.data = {
24
+ name: 'base-command',
25
+ description: 'Base command description',
26
+ cooldown: 10,
27
+ permissions: [],
28
+ ownerOnly: false,
29
+ teamOnly: false,
30
+ guildOnly: false,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Main command execution method (must be implemented by child classes)
36
+ * @param {import('discord.js').CommandInteraction} interaction - The interaction object
37
+ * @returns {Promise<void>}
38
+ */
39
+ async execute(interaction) {
40
+ if (interaction.options?.getSubcommand?.(false)) {
41
+ this.logger.warn(`Command group ${this.constructor.name} execute called - this should be handled by subcommand`);
42
+ return;
43
+ }
44
+
45
+ throw new Error(`Execute method not implemented for ${this.constructor.name}`);
46
+ }
47
+ }
48
+
49
+ module.exports = BaseCommand;
@@ -0,0 +1,176 @@
1
+ const discordColors = {
2
+ Default: 0x000000,
3
+ White: 0xffffff,
4
+ Aqua: 0x1abc9c,
5
+ Green: 0x57f287,
6
+ Blue: 0x3498db,
7
+ Yellow: 0xfee75c,
8
+ Purple: 0x9b59b6,
9
+ LuminousVividPink: 0xe91e63,
10
+ Fuchsia: 0xeb459e,
11
+ Gold: 0xf1c40f,
12
+ Orange: 0xe67e22,
13
+ Red: 0xed4245,
14
+ Grey: 0x95a5a6,
15
+ Navy: 0x34495e,
16
+ DarkAqua: 0x11806a,
17
+ DarkGreen: 0x1f8b4c,
18
+ DarkBlue: 0x206694,
19
+ DarkPurple: 0x71368a,
20
+ DarkVividPink: 0xad1457,
21
+ DarkGold: 0xc27c0e,
22
+ DarkOrange: 0xa84300,
23
+ DarkRed: 0x992d22,
24
+ DarkGrey: 0x979c9f,
25
+ DarkerGrey: 0x7f8c8d,
26
+ LightGrey: 0xbcc0c0,
27
+ DarkNavy: 0x2c3e50,
28
+ Blurple: 0x5865f2,
29
+ Greyple: 0x99aab5,
30
+ DarkButNotBlack: 0x2c2f33,
31
+ NotQuiteBlack: 0x23272a,
32
+ };
33
+
34
+ /**
35
+ * Converts a color between multiple representations.
36
+ *
37
+ * @param {string|number|{r:number,g:number,b:number}} input - The input color value.
38
+ * @param {{from:'hex'|'rgb'|'decimal'|'discord', to:'hex'|'rgb'|'decimal'}} options - Conversion options.
39
+ * @returns {string|number|{r:number,g:number,b:number}} The converted color.
40
+ */
41
+ function convertColor(input, { from, to }) {
42
+ function hexToRgb(hex) {
43
+ let h = hex.replace(/^#/, '');
44
+ if (h.length === 3) {
45
+ h = h
46
+ .split('')
47
+ .map((x) => x + x)
48
+ .join('');
49
+ }
50
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) throw new Error('Invalid hex color');
51
+ return {
52
+ r: parseInt(h.slice(0, 2), 16),
53
+ g: parseInt(h.slice(2, 4), 16),
54
+ b: parseInt(h.slice(4, 6), 16),
55
+ };
56
+ }
57
+
58
+ function rgbToHex({ r, g, b }) {
59
+ if (
60
+ typeof r !== 'number' ||
61
+ typeof g !== 'number' ||
62
+ typeof b !== 'number' ||
63
+ r < 0 ||
64
+ r > 255 ||
65
+ g < 0 ||
66
+ g > 255 ||
67
+ b < 0 ||
68
+ b > 255
69
+ )
70
+ throw new Error('Invalid RGB color');
71
+ return (
72
+ '#' +
73
+ [r, g, b]
74
+ .map((x) => {
75
+ const hex = x.toString(16);
76
+ return hex.length === 1 ? '0' + hex : hex;
77
+ })
78
+ .join('')
79
+ .toUpperCase()
80
+ );
81
+ }
82
+
83
+ function hexToDecimal(hex) {
84
+ let h = hex.replace(/^#/, '');
85
+ if (h.length === 3) {
86
+ h = h
87
+ .split('')
88
+ .map((x) => x + x)
89
+ .join('');
90
+ }
91
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) throw new Error('Invalid hex color');
92
+ return Number('0x' + h.toUpperCase());
93
+ }
94
+
95
+ function decimalToHex(decimal) {
96
+ if (typeof decimal !== 'number' || decimal < 0 || decimal > 0xffffff) throw new Error('Invalid decimal color');
97
+ let hex = decimal.toString(16).toUpperCase();
98
+ while (hex.length < 6) hex = '0' + hex;
99
+ return '#' + hex;
100
+ }
101
+
102
+ function rgbToDecimal({ r, g, b }) {
103
+ if (
104
+ typeof r !== 'number' ||
105
+ typeof g !== 'number' ||
106
+ typeof b !== 'number' ||
107
+ r < 0 ||
108
+ r > 255 ||
109
+ g < 0 ||
110
+ g > 255 ||
111
+ b < 0 ||
112
+ b > 255
113
+ )
114
+ throw new Error('Invalid RGB color');
115
+ return (r << 16) + (g << 8) + b;
116
+ }
117
+
118
+ function decimalToRgb(decimal) {
119
+ if (typeof decimal !== 'number' || decimal < 0 || decimal > 0xffffff) throw new Error('Invalid decimal color');
120
+ return {
121
+ r: (decimal >> 16) & 0xff,
122
+ g: (decimal >> 8) & 0xff,
123
+ b: decimal & 0xff,
124
+ };
125
+ }
126
+
127
+ if (from === to) return input;
128
+
129
+ let rgb, hex, decimal;
130
+
131
+ switch (from) {
132
+ case 'hex':
133
+ hex = input;
134
+ rgb = hexToRgb(hex);
135
+ decimal = hexToDecimal(hex);
136
+ break;
137
+ case 'rgb':
138
+ rgb = input;
139
+ hex = rgbToHex(rgb);
140
+ decimal = rgbToDecimal(rgb);
141
+ break;
142
+ case 'decimal':
143
+ decimal = input;
144
+ hex = decimalToHex(decimal);
145
+ rgb = decimalToRgb(decimal);
146
+ break;
147
+ case 'discord':
148
+ if (typeof input === 'string') {
149
+ const key = Object.keys(discordColors).find((k) => k.toLowerCase() === input.toLowerCase());
150
+ if (!key) throw new Error(`Invalid Discord color name: ${input}`);
151
+ decimal = discordColors[key];
152
+ } else if (typeof input === 'number') {
153
+ decimal = input;
154
+ } else {
155
+ throw new Error('Invalid input type for Discord color');
156
+ }
157
+ hex = decimalToHex(decimal);
158
+ rgb = decimalToRgb(decimal);
159
+ break;
160
+ default:
161
+ throw new Error(`Invalid "from" color type: ${from}`);
162
+ }
163
+
164
+ switch (to) {
165
+ case 'hex':
166
+ return hex;
167
+ case 'rgb':
168
+ return rgb;
169
+ case 'decimal':
170
+ return decimal;
171
+ default:
172
+ throw new Error(`Invalid "to" color type: ${to}`);
173
+ }
174
+ }
175
+
176
+ module.exports = convertColor;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Converts a string to a small-caps-like Unicode alphabet variant.
3
+ * @param {string} text - Input text.
4
+ * @returns {string} Converted text using tiny letters.
5
+ */
6
+ function toTinyText(text) {
7
+ const normal = 'abcdefghijklmnopqrstuvwxyz';
8
+ const tiny = [
9
+ 'แด€',
10
+ 'ส™',
11
+ 'แด„',
12
+ 'แด…',
13
+ 'แด‡',
14
+ 'า“',
15
+ 'ษข',
16
+ 'สœ',
17
+ 'ษช',
18
+ 'แดŠ',
19
+ 'แด‹',
20
+ 'สŸ',
21
+ 'แด',
22
+ 'ษด',
23
+ 'แด',
24
+ 'แด˜',
25
+ 'วซ',
26
+ 'ส€',
27
+ 's',
28
+ 'แด›',
29
+ 'แดœ',
30
+ 'แด ',
31
+ 'แดก',
32
+ 'x',
33
+ 'ส',
34
+ 'แดข',
35
+ ];
36
+
37
+ return text
38
+ .split('')
39
+ .map((char) => {
40
+ const lowerChar = char.toLowerCase();
41
+ const index = normal.indexOf(lowerChar);
42
+ if (index !== -1) {
43
+ return tiny[index];
44
+ }
45
+ return char;
46
+ })
47
+ .join('');
48
+ }
49
+
50
+ /**
51
+ * Converts a string to a bold Unicode alphabet variant approximating "tiny" bold.
52
+ * @param {string} text - Input text.
53
+ * @returns {string} Converted text using bold tiny letters.
54
+ */
55
+ function toTinyBoldText(text) {
56
+ const normal = 'abcdefghijklmnopqrstuvwxyz';
57
+ const tinyBold = [
58
+ '๐—ฎ',
59
+ '๐—ฏ',
60
+ '๐—ฐ',
61
+ '๐—ฑ',
62
+ '๐—ฒ',
63
+ '๐—ณ',
64
+ '๐—ด',
65
+ '๐—ต',
66
+ '๐—ถ',
67
+ '๐—ท',
68
+ '๐—ธ',
69
+ '๐—น',
70
+ '๐—บ',
71
+ '๐—ป',
72
+ '๐—ผ',
73
+ '๐—ฝ',
74
+ '๐—พ',
75
+ '๐—ฟ',
76
+ '๐˜€',
77
+ '๐˜',
78
+ '๐˜‚',
79
+ '๐˜ƒ',
80
+ '๐˜„',
81
+ '๐˜…',
82
+ '๐˜†',
83
+ '๐˜‡',
84
+ ];
85
+
86
+ return text
87
+ .split('')
88
+ .map((char) => {
89
+ const lowerChar = char.toLowerCase();
90
+ const index = normal.indexOf(lowerChar);
91
+ if (index !== -1) {
92
+ return tinyBold[index];
93
+ }
94
+ return char;
95
+ })
96
+ .join('');
97
+ }
98
+
99
+ module.export = { toTinyText, toTinyBoldText };
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ color: require('./color.js'),
3
+ formatter: require('./formatter.js'),
4
+ };