kythia-core 0.9.4-beta.3 → 0.9.5-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/changelog.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.9.5-beta](https://github.com/kenndeclouv/kythia-core/compare/v0.9.4-beta.3...v0.9.5-beta) (2025-11-11)
6
+
7
+
8
+ ### ✨ Added
9
+
10
+ * Add select menu handler registration and interaction handling in Kythia and InteractionManager, enhancing user interaction capabilities. ([a27e84d](https://github.com/kenndeclouv/kythia-core/commit/a27e84d60f6f6077b073582f7c705eb28bc4d329))
11
+
5
12
  ### [0.9.4-beta.3](https://github.com/kenndeclouv/kythia-core/compare/v0.9.4-beta.2...v0.9.4-beta.3) (2025-11-10)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kythia-core",
3
- "version": "0.9.4-beta.3",
3
+ "version": "0.9.5-beta",
4
4
  "description": "Core library for the Kythia main Discord bot: extensible, modular, and scalable foundation for commands, components, and event management.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/Kythia.js CHANGED
@@ -4,7 +4,7 @@
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.9.5-beta
8
8
  *
9
9
  * @description
10
10
  * This file contains the main Bot class - acting as an orchestrator (CEO) that
@@ -187,6 +187,17 @@ class Kythia {
187
187
  this.addonManager.registerModalHandler(customIdPrefix, handler);
188
188
  }
189
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
+ }
190
201
 
191
202
  /**
192
203
  * 🛡️ Validate License (Stub)
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaModel.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.4-beta.3
7
+ * @version 0.9.5-beta
8
8
  *
9
9
  * @description
10
10
  * Caching layer for Sequelize Models, now sharding-aware. When config.db.redis.shard === true,
@@ -800,43 +800,89 @@ class KythiaModel extends Model {
800
800
  return queryPromise;
801
801
  }
802
802
 
803
+
803
804
  /**
804
- * 📦 Attempts to find a record based on `options.where`. If found, it returns the cached or DB record.
805
+ * 📦 Finds a record by the specified where condition, using cache if available; if not found, creates it and caches the result.
806
+ * Will update an existing cached instance with defaults (if necessary) and save any new/changed data to both DB and cache.
805
807
  */
806
808
  static async findOrCreateWithCache(options) {
807
809
  if (!options || !options.where) {
808
810
  throw new Error("findOrCreateWithCache requires a 'where' option.");
809
811
  }
810
812
 
811
- const { cacheTags, noCache, ...findOrCreateOptions } = options;
813
+ const { where, defaults, cacheTags, noCache, ...otherOptions } = options;
814
+
815
+ if (noCache) {
816
+ return this.findOrCreate(options);
817
+ }
818
+
819
+ const normalizedWhere = this._normalizeFindOptions(where).where;
820
+ const cacheKey = this.getCacheKey(normalizedWhere);
821
+
822
+ const cacheResult = await this.getCachedEntry(cacheKey, otherOptions.include);
812
823
 
813
- const cacheKey = this.getCacheKey(options.where);
814
- const cacheResult = await this.getCachedEntry(cacheKey);
815
824
  if (cacheResult.hit && cacheResult.data) {
816
- return [cacheResult.data, false];
825
+ const instance = cacheResult.data;
826
+ let needsUpdate = false;
827
+
828
+ if (defaults && typeof defaults === 'object') {
829
+ for (const key in defaults) {
830
+ if (instance[key] === undefined || String(instance[key]) !== String(defaults[key])) {
831
+ instance[key] = defaults[key];
832
+ needsUpdate = true;
833
+ }
834
+ }
835
+ }
836
+
837
+ if (needsUpdate) {
838
+ await instance.saveAndUpdateCache();
839
+ }
840
+
841
+ return [instance, false];
817
842
  }
843
+
818
844
  this.cacheStats.misses++;
819
845
  if (this.pendingQueries.has(cacheKey)) {
820
846
  return this.pendingQueries.get(cacheKey);
821
847
  }
822
- const findOrCreatePromise = this.findOrCreate(findOrCreateOptions)
823
- .then(([instance, created]) => {
824
- if (this.isRedisConnected || !this.isShardMode) {
825
- const tags = [`${this.name}`];
826
- if (instance) {
827
- const pk = this.primaryKeyAttribute;
828
- tags.push(`${this.name}:${pk}:${instance[pk]}`);
848
+
849
+ const findPromise = this.findOne({ where, ...otherOptions })
850
+ .then(async (instance) => {
851
+ if (instance) {
852
+ let needsUpdate = false;
853
+ if (defaults && typeof defaults === 'object') {
854
+ for (const key in defaults) {
855
+ if (instance[key] === undefined || String(instance[key]) !== String(defaults[key])) {
856
+ instance[key] = defaults[key];
857
+ needsUpdate = true;
858
+ }
859
+ }
829
860
  }
830
- this.setCacheEntry(cacheKey, instance, undefined, tags);
861
+
862
+ if (needsUpdate) {
863
+ await instance.saveAndUpdateCache();
864
+ } else {
865
+ const tags = [`${this.name}`, `${this.name}:${this.primaryKeyAttribute}:${instance[this.primaryKeyAttribute]}`];
866
+ await this.setCacheEntry(cacheKey, instance, undefined, tags);
867
+ }
868
+
869
+ return [instance, false];
870
+ } else {
871
+ const createData = { ...where, ...defaults };
872
+ const newInstance = await this.create(createData);
873
+
874
+ const tags = [`${this.name}`, `${this.name}:${this.primaryKeyAttribute}:${newInstance[this.primaryKeyAttribute]}`];
875
+ await this.setCacheEntry(cacheKey, newInstance, undefined, tags);
876
+
877
+ return [newInstance, true];
831
878
  }
832
- return [instance, created];
833
879
  })
834
880
  .finally(() => {
835
881
  this.pendingQueries.delete(cacheKey);
836
882
  });
837
883
 
838
- this.pendingQueries.set(cacheKey, findOrCreatePromise);
839
- return findOrCreatePromise;
884
+ this.pendingQueries.set(cacheKey, findPromise);
885
+ return findPromise;
840
886
  }
841
887
 
842
888
  /**
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaORM.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.4-beta.3
7
+ * @version 0.9.5-beta
8
8
  *
9
9
  * @description
10
10
  * A utility for intelligent, hash-based syncing of Sequelize models.
@@ -4,7 +4,7 @@
4
4
  * @file src/database/KythiaSequelize.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.4-beta.3
7
+ * @version 0.9.5-beta
8
8
  *
9
9
  * @description
10
10
  * Main Sequelize connection factory for the application.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/AddonManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.4-beta.3
7
+ * @version 0.9.5-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all addon loading, command registration, and component management.
@@ -57,6 +57,18 @@ class AddonManager {
57
57
  }
58
58
  this.buttonHandlers.set(customId, handler);
59
59
  }
60
+ /**
61
+ * 🔽 Register Select Menu Handler
62
+ * Registers a handler function for a specific select menu customId prefix.
63
+ * @param {string} customIdPrefix - The prefix of the select menu customId
64
+ * @param {Function} handler - The handler function to execute
65
+ */
66
+ registerSelectMenuHandler(customIdPrefix, handler) {
67
+ if (this.selectMenuHandlers.has(customIdPrefix)) {
68
+ this.logger.warn(`[REGISTRATION] Warning: Select menu handler for [${customIdPrefix}] already exists and will be overwritten.`);
69
+ }
70
+ this.selectMenuHandlers.set(customIdPrefix, handler);
71
+ }
60
72
 
61
73
  /**
62
74
  * 📝 Register Modal Handler
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/EventManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.4-beta.3
7
+ * @version 0.9.5-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord event listeners except InteractionCreate.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/InteractionManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.4-beta.3
7
+ * @version 0.9.5-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord interaction events including slash commands, buttons, modals,
@@ -77,6 +77,8 @@ class InteractionManager {
77
77
  await this._handleButton(interaction);
78
78
  } else if (interaction.isModalSubmit()) {
79
79
  await this._handleModalSubmit(interaction);
80
+ } else if (interaction.isAnySelectMenu()) {
81
+ await this._handleSelectMenu(interaction);
80
82
  } else if (interaction.isUserContextMenuCommand() || interaction.isMessageContextMenuCommand()) {
81
83
  await this._handleContextMenuCommand(interaction, formatPerms);
82
84
  }
@@ -222,11 +224,10 @@ class InteractionManager {
222
224
  }
223
225
 
224
226
  if (typeof command.execute === 'function') {
225
- // Ensure logger is defined for the command execution context
226
227
  if (!interaction.logger) {
227
228
  interaction.logger = this.logger;
228
229
  }
229
- // Also inject logger into the container for legacy/compatibility
230
+
230
231
  if (this.container && !this.container.logger) {
231
232
  this.container.logger = this.logger;
232
233
  }
@@ -280,17 +281,21 @@ class InteractionManager {
280
281
  * @private
281
282
  */
282
283
  async _handleButton(interaction) {
283
- const customIdPrefix = interaction.customId.includes('|')
284
- ? interaction.customId.split('|')[0]
285
- : interaction.customId.split(':')[0];
284
+ const customIdPrefix = interaction.customId.includes('|') ? interaction.customId.split('|')[0] : interaction.customId.split(':')[0];
286
285
 
287
- const handler = this.buttonHandlers.get(customIdPrefix);
286
+ const handler = this.buttonHandlers.get(customIdPrefix);
288
287
 
289
288
  if (handler) {
290
- if (handler.length === 2) {
291
- await handler(interaction, this.container);
289
+ if (typeof handler === 'object' && typeof handler.execute === 'function') {
290
+ await handler.execute(interaction, this.container);
291
+ } else if (typeof handler === 'function') {
292
+ if (handler.length === 2) {
293
+ await handler(interaction, this.container);
294
+ } else {
295
+ await handler(interaction);
296
+ }
292
297
  } else {
293
- await handler(interaction);
298
+ this.logger.error(`Handler for button ${customIdPrefix} has an invalid format`);
294
299
  }
295
300
  }
296
301
  }
@@ -301,10 +306,52 @@ class InteractionManager {
301
306
  */
302
307
  async _handleModalSubmit(interaction) {
303
308
  const customIdPrefix = interaction.customId.includes('|') ? interaction.customId.split('|')[0] : interaction.customId.split(':')[0];
304
- this.logger.info('Modal submit - customId:', interaction.customId, 'prefix:', customIdPrefix);
309
+
310
+ this.logger.info(`Modal submit - customId: ${interaction.customId}, prefix: ${customIdPrefix}`);
311
+
305
312
  const handler = this.modalHandlers.get(customIdPrefix);
306
- this.logger.info('Modal handler found:', !!handler);
307
- if (handler) await handler(interaction, this.container);
313
+ this.logger.info(`Modal handler found: ${!!handler}`);
314
+
315
+ if (handler) {
316
+ if (typeof handler === 'object' && typeof handler.execute === 'function') {
317
+ await handler.execute(interaction, this.container);
318
+ } else if (typeof handler === 'function') {
319
+ if (handler.length === 2) {
320
+ await handler(interaction, this.container);
321
+ } else {
322
+ await handler(interaction);
323
+ }
324
+ } else {
325
+ this.logger.error(`Handler untuk modal ${customIdPrefix} formatnya salah (bukan fungsi atau { execute: ... })`);
326
+ }
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Handle select menu interactions
332
+ * @private
333
+ */
334
+ async _handleSelectMenu(interaction) {
335
+ const customIdPrefix = interaction.customId.includes('|') ? interaction.customId.split('|')[0] : interaction.customId.split(':')[0];
336
+
337
+ this.logger.info(`Select menu submit - customId: ${interaction.customId}, prefix: ${customIdPrefix}`);
338
+
339
+ const handler = this.selectMenuHandlers.get(customIdPrefix);
340
+ this.logger.info(`Select menu handler found: ${!!handler}`);
341
+
342
+ if (handler) {
343
+ if (typeof handler === 'object' && typeof handler.execute === 'function') {
344
+ await handler.execute(interaction, this.container);
345
+ } else if (typeof handler === 'function') {
346
+ if (handler.length === 2) {
347
+ await handler(interaction, this.container);
348
+ } else {
349
+ await handler(interaction);
350
+ }
351
+ } else {
352
+ this.logger.error(`Handler untuk select menu ${customIdPrefix} formatnya salah`);
353
+ }
354
+ }
308
355
  }
309
356
 
310
357
  /**
@@ -417,7 +464,6 @@ class InteractionManager {
417
464
  }
418
465
  }
419
466
 
420
- // Ensure logger is available in the execution context
421
467
  if (!interaction.logger) {
422
468
  interaction.logger = this.logger;
423
469
  }
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/ShutdownManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.4-beta.3
7
+ * @version 0.9.5-beta
8
8
  *
9
9
  * @description
10
10
  * Handles graceful shutdown procedures including interval tracking,