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.
@@ -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.5-beta
7
+ * @version 0.11.0-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all Discord interaction events including slash commands, buttons, modals,
@@ -12,599 +12,804 @@
12
12
  */
13
13
 
14
14
  const {
15
- Events,
16
- Collection,
17
- ButtonStyle,
18
- MessageFlags,
19
- EmbedBuilder,
20
- ButtonBuilder,
21
- WebhookClient,
22
- SeparatorBuilder,
23
- ActionRowBuilder,
24
- ContainerBuilder,
25
- TextDisplayBuilder,
26
- SeparatorSpacingSize,
15
+ Events,
16
+ Collection,
17
+ ButtonStyle,
18
+ MessageFlags,
19
+ EmbedBuilder,
20
+ ButtonBuilder,
21
+ WebhookClient,
22
+ SeparatorBuilder,
23
+ ActionRowBuilder,
24
+ ContainerBuilder,
25
+ TextDisplayBuilder,
26
+ SeparatorSpacingSize,
27
27
  } = require('discord.js');
28
28
  const convertColor = require('../utils/color');
29
29
  const Sentry = require('@sentry/node');
30
30
 
31
31
  class InteractionManager {
32
- /**
33
- * 🏗️ InteractionManager Constructor
34
- * @param {Object} client - Discord client instance
35
- * @param {Object} container - Dependency container
36
- * @param {Object} handlers - Handler maps from AddonManager
37
- */
38
- constructor({ client, container, handlers }) {
39
- this.client = client;
40
- this.container = container;
41
- this.buttonHandlers = handlers.buttonHandlers;
42
- this.modalHandlers = handlers.modalHandlers;
43
- this.selectMenuHandlers = handlers.selectMenuHandlers;
44
- this.autocompleteHandlers = handlers.autocompleteHandlers;
45
- this.commandCategoryMap = handlers.commandCategoryMap;
46
- this.categoryToFeatureMap = handlers.categoryToFeatureMap;
47
-
48
- this.kythiaConfig = this.container.kythiaConfig;
49
- this.models = this.container.models;
50
- this.helpers = this.container.helpers;
51
-
52
- this.logger = this.container.logger;
53
- this.t = this.container.t;
54
-
55
- this.ServerSetting = this.models.ServerSetting;
56
- this.KythiaVoter = this.models.KythiaVoter;
57
- this.isTeam = this.helpers.discord.isTeam;
58
- this.isOwner = this.helpers.discord.isOwner;
59
- }
60
-
61
- /**
62
- * 🛎️ Initialize Interaction Handler
63
- * Sets up the main Discord interaction handler for commands, autocomplete, buttons, and modals.
64
- */
65
- initialize() {
66
- function formatPerms(permsArray) {
67
- return permsArray.map((perm) => perm.replace(/([A-Z])/g, ' $1').trim()).join(', ');
68
- }
69
-
70
- this.client.on(Events.InteractionCreate, async (interaction) => {
71
- try {
72
- if (interaction.isChatInputCommand()) {
73
- await this._handleChatInputCommand(interaction, formatPerms);
74
- } else if (interaction.isAutocomplete()) {
75
- await this._handleAutocomplete(interaction);
76
- } else if (interaction.isButton()) {
77
- await this._handleButton(interaction);
78
- } else if (interaction.isModalSubmit()) {
79
- await this._handleModalSubmit(interaction);
80
- } else if (interaction.isAnySelectMenu()) {
81
- await this._handleSelectMenu(interaction);
82
- } else if (interaction.isUserContextMenuCommand() || interaction.isMessageContextMenuCommand()) {
83
- await this._handleContextMenuCommand(interaction, formatPerms);
84
- }
85
- } catch (error) {
86
- await this._handleInteractionError(interaction, error);
87
- }
88
- });
89
-
90
- this.client.on(Events.AutoModerationActionExecution, async (execution) => {
91
- try {
92
- await this._handleAutoModerationAction(execution);
93
- } catch (err) {
94
- this.logger.error(`[AutoMod Logger] Error during execution for ${execution.guild.name}:`, err);
95
- }
96
- });
97
- }
98
-
99
- /**
100
- * Handle chat input commands
101
- * @private
102
- */
103
- async _handleChatInputCommand(interaction, formatPerms) {
104
- let commandKey = interaction.commandName;
105
- const group = interaction.options.getSubcommandGroup(false);
106
- const subcommand = interaction.options.getSubcommand(false);
107
-
108
- if (group) commandKey = `${commandKey} ${group} ${subcommand}`;
109
- else if (subcommand) commandKey = `${commandKey} ${subcommand}`;
110
-
111
- let command = this.client.commands.get(commandKey);
112
-
113
- if (!command && (subcommand || group)) {
114
- command = this.client.commands.get(interaction.commandName);
115
- }
116
- if (!command) {
117
- this.logger.error(`Command not found for key: ${commandKey}`);
118
- return interaction.reply({ content: await this.t(interaction, 'common.error.command.not.found'), ephemeral: true });
119
- }
120
-
121
- if (interaction.inGuild()) {
122
- const category = this.commandCategoryMap.get(interaction.commandName);
123
- const featureFlag = this.categoryToFeatureMap.get(category);
124
-
125
- if (featureFlag && !this.isOwner(interaction.user.id)) {
126
- const settings = await this.ServerSetting.getCache({ guildId: interaction.guild.id });
127
-
128
- if (settings && Object.prototype.hasOwnProperty.call(settings, featureFlag) && settings[featureFlag] === false) {
129
- const featureName = category.charAt(0).toUpperCase() + category.slice(1);
130
- const reply = await this.t(interaction, 'common.error.feature.disabled', { feature: featureName });
131
- return interaction.reply({ content: reply });
132
- }
133
- }
134
- }
135
-
136
- if (command.guildOnly && !interaction.inGuild()) {
137
- return interaction.reply({ content: await this.t(interaction, 'common.error.guild.only'), ephemeral: true });
138
- }
139
- if (command.ownerOnly && !this.isOwner(interaction.user.id)) {
140
- return interaction.reply({ content: await this.t(interaction, 'common.error.not.owner'), ephemeral: true });
141
- }
142
- if (command.teamOnly && !this.isOwner(interaction.user.id)) {
143
- const isTeamMember = await this.isTeam(interaction.user);
144
- if (!isTeamMember) return interaction.reply({ content: await this.t(interaction, 'common.error.not.team'), ephemeral: true });
145
- }
146
- if (command.permissions && interaction.inGuild()) {
147
- const missingPerms = interaction.member.permissions.missing(command.permissions);
148
- if (missingPerms.length > 0)
149
- return interaction.reply({
150
- content: await this.t(interaction, 'common.error.user.missing.perms', { perms: formatPerms(missingPerms) }),
151
- ephemeral: true,
152
- });
153
- }
154
- if (command.botPermissions && interaction.inGuild()) {
155
- const missingPerms = interaction.guild.members.me.permissions.missing(command.botPermissions);
156
- if (missingPerms.length > 0)
157
- return interaction.reply({
158
- content: await this.t(interaction, 'common.error.bot.missing.perms', { perms: formatPerms(missingPerms) }),
159
- ephemeral: true,
160
- });
161
- }
162
-
163
- if (command.voteLocked && !this.isOwner(interaction.user.id)) {
164
- const voter = await this.KythiaVoter.getCache({ userId: interaction.user.id });
165
- const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000);
166
-
167
- if (!voter || voter.votedAt < twelveHoursAgo) {
168
- const container = new ContainerBuilder().setAccentColor(
169
- convertColor(this.kythiaConfig.bot.color, { from: 'hex', to: 'decimal' })
170
- );
171
- container.addTextDisplayComponents(
172
- new TextDisplayBuilder().setContent(await this.t(interaction, 'common.error.vote.locked.text'))
173
- );
174
- container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
175
- container.addActionRowComponents(
176
- new ActionRowBuilder().addComponents(
177
- new ButtonBuilder()
178
- .setLabel(
179
- await this.t(interaction, 'common.error.vote.locked.button', {
180
- botName: interaction.client.user.username,
181
- })
182
- )
183
- .setStyle(ButtonStyle.Link)
184
- .setURL(`https://top.gg/bot/${this.kythiaConfig.bot.clientId}/vote`)
185
- )
186
- );
187
- container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
188
- container.addTextDisplayComponents(
189
- new TextDisplayBuilder().setContent(await this.t(interaction, 'common.container.footer'))
190
- );
191
- return interaction.reply({
192
- components: [container],
193
- ephemeral: true,
194
- flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
195
- });
196
- }
197
- }
198
-
199
- const cooldownDuration = command.cooldown ?? this.kythiaConfig.bot.globalCommandCooldown ?? 0;
200
-
201
- if (cooldownDuration > 0 && !this.isOwner(interaction.user.id)) {
202
- const { cooldowns } = this.client;
203
-
204
- if (!cooldowns.has(command.name)) {
205
- cooldowns.set(command.name, new Collection());
206
- }
207
-
208
- const now = Date.now();
209
- const timestamps = cooldowns.get(command.name);
210
- const cooldownAmount = cooldownDuration * 1000;
211
-
212
- if (timestamps.has(interaction.user.id)) {
213
- const expirationTime = timestamps.get(interaction.user.id) + cooldownAmount;
214
-
215
- if (now < expirationTime) {
216
- const timeLeft = (expirationTime - now) / 1000;
217
- const reply = await this.t(interaction, 'common.error.cooldown', { time: timeLeft.toFixed(1) });
218
- return interaction.reply({ content: reply, ephemeral: true });
219
- }
220
- }
221
-
222
- timestamps.set(interaction.user.id, now);
223
- setTimeout(() => timestamps.delete(interaction.user.id), cooldownAmount);
224
- }
225
-
226
- if (typeof command.execute === 'function') {
227
- if (!interaction.logger) {
228
- interaction.logger = this.logger;
229
- }
230
-
231
- if (this.container && !this.container.logger) {
232
- this.container.logger = this.logger;
233
- }
234
- if (command.execute.length === 2) {
235
- await command.execute(interaction, this.container);
236
- } else {
237
- await command.execute(interaction);
238
- }
239
- } else {
240
- this.logger.error("Command doesn't have a valid 'execute' function:", command.name || commandKey);
241
- return interaction.reply({ content: await this.t(interaction, 'common.error.command.execution.invalid'), ephemeral: true });
242
- }
243
- }
244
-
245
- /**
246
- * Handle autocomplete interactions
247
- * @private
248
- */
249
- async _handleAutocomplete(interaction) {
250
- let commandKey = interaction.commandName;
251
- const group = interaction.options.getSubcommandGroup(false);
252
- const subcommand = interaction.options.getSubcommand(false);
253
-
254
- if (group) commandKey = `${commandKey} ${group} ${subcommand}`;
255
- else if (subcommand) commandKey = `${commandKey} ${subcommand}`;
256
-
257
- let handler = this.autocompleteHandlers.get(commandKey);
258
-
259
- if (!handler && (subcommand || group)) {
260
- handler = this.autocompleteHandlers.get(interaction.commandName);
261
- }
262
-
263
- if (handler) {
264
- try {
265
- await handler(interaction, this.container);
266
- } catch (err) {
267
- this.logger.error(`Error in autocomplete handler for ${commandKey}:`, err);
268
- try {
269
- await interaction.respond([]);
270
- } catch (e) {}
271
- }
272
- } else {
273
- try {
274
- await interaction.respond([]);
275
- } catch (e) {}
276
- }
277
- }
278
-
279
- /**
280
- * Handle button interactions
281
- * @private
282
- */
283
- async _handleButton(interaction) {
284
- const customIdPrefix = interaction.customId.includes('|') ? interaction.customId.split('|')[0] : interaction.customId.split(':')[0];
285
-
286
- const handler = this.buttonHandlers.get(customIdPrefix);
287
-
288
- if (handler) {
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
- }
297
- } else {
298
- this.logger.error(`Handler for button ${customIdPrefix} has an invalid format`);
299
- }
300
- }
301
- }
302
-
303
- /**
304
- * Handle modal submit interactions
305
- * @private
306
- */
307
- async _handleModalSubmit(interaction) {
308
- const customIdPrefix = interaction.customId.includes('|') ? interaction.customId.split('|')[0] : interaction.customId.split(':')[0];
309
-
310
- this.logger.info(`Modal submit - customId: ${interaction.customId}, prefix: ${customIdPrefix}`);
311
-
312
- const handler = this.modalHandlers.get(customIdPrefix);
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
- }
355
- }
356
-
357
- /**
358
- * Handle context menu commands
359
- * @private
360
- */
361
- async _handleContextMenuCommand(interaction, formatPerms) {
362
- const command = this.client.commands.get(interaction.commandName);
363
- if (!command) return;
364
-
365
- if (command.guildOnly && !interaction.inGuild()) {
366
- return interaction.reply({ content: await this.t(interaction, 'common.error.guild.only'), ephemeral: true });
367
- }
368
- if (command.ownerOnly && !this.isOwner(interaction.user.id)) {
369
- return interaction.reply({ content: await this.t(interaction, 'common.error.not.owner'), ephemeral: true });
370
- }
371
- if (command.teamOnly && !this.isOwner(interaction.user.id)) {
372
- const isTeamMember = await this.isTeam(interaction.user);
373
- if (!isTeamMember) return interaction.reply({ content: await this.t(interaction, 'common.error.not.team'), ephemeral: true });
374
- }
375
- if (command.permissions && interaction.inGuild()) {
376
- const missingPerms = interaction.member.permissions.missing(command.permissions);
377
- if (missingPerms.length > 0)
378
- return interaction.reply({
379
- content: await this.t(interaction, 'common.error.user.missing.perms', { perms: formatPerms(missingPerms) }),
380
- ephemeral: true,
381
- });
382
- }
383
- if (command.botPermissions && interaction.inGuild()) {
384
- const missingPerms = interaction.guild.members.me.permissions.missing(command.botPermissions);
385
- if (missingPerms.length > 0)
386
- return interaction.reply({
387
- content: await this.t(interaction, 'common.error.bot.missing.perms', { perms: formatPerms(missingPerms) }),
388
- ephemeral: true,
389
- });
390
- }
391
- if (command.isInMainGuild && !this.isOwner(interaction.user.id)) {
392
- const mainGuild = this.client.guilds.cache.get(this.kythiaConfig.bot.mainGuildId);
393
- if (!mainGuild) {
394
- this.logger.error(
395
- `❌ [isInMainGuild Check] Error: Bot is not a member of the main guild specified in config: ${this.kythiaConfig.bot.mainGuildId}`
396
- );
397
- }
398
- try {
399
- await mainGuild.members.fetch(interaction.user.id);
400
- } catch (error) {
401
- const container = new ContainerBuilder().setAccentColor(
402
- convertColor(this.kythiaConfig.bot.color, { from: 'hex', to: 'decimal' })
403
- );
404
- container.addTextDisplayComponents(
405
- new TextDisplayBuilder().setContent(
406
- await this.t(interaction, 'common.error.not.in.main.guild.text', { name: mainGuild.name })
407
- )
408
- );
409
- container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
410
- container.addActionRowComponents(
411
- new ActionRowBuilder().addComponents(
412
- new ButtonBuilder()
413
- .setLabel(await this.t(interaction, 'common.error.not.in.main.guild.button.join'))
414
- .setStyle(ButtonStyle.Link)
415
- .setURL(this.kythiaConfig.settings.supportServer)
416
- )
417
- );
418
- container.addTextDisplayComponents(
419
- new TextDisplayBuilder().setContent(
420
- await this.t(interaction, 'common.container.footer', { username: interaction.client.user.username })
421
- )
422
- );
423
- return interaction.reply({
424
- components: [container],
425
- flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
426
- });
427
- }
428
- }
429
- if (command.voteLocked && !this.isOwner(interaction.user.id)) {
430
- const voter = await this.KythiaVoter.getCache({ userId: interaction.user.id });
431
- const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000);
432
-
433
- if (!voter || voter.votedAt < twelveHoursAgo) {
434
- const container = new ContainerBuilder().setAccentColor(
435
- convertColor(this.kythiaConfig.bot.color, { from: 'hex', to: 'decimal' })
436
- );
437
- container.addTextDisplayComponents(
438
- new TextDisplayBuilder().setContent(await this.t(interaction, 'common.error.vote.locked.text'))
439
- );
440
- container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
441
- container.addActionRowComponents(
442
- new ActionRowBuilder().addComponents(
443
- new ButtonBuilder()
444
- .setLabel(
445
- await this.t(interaction, 'common.error.vote.locked.button', {
446
- username: interaction.client.user.username,
447
- })
448
- )
449
- .setStyle(ButtonStyle.Link)
450
- .setURL(`https://top.gg/bot/${this.kythiaConfig.bot.clientId}/vote`)
451
- )
452
- );
453
- container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
454
- container.addTextDisplayComponents(
455
- new TextDisplayBuilder().setContent(
456
- await this.t(interaction, 'common.container.footer', { username: interaction.client.user.username })
457
- )
458
- );
459
- return interaction.reply({
460
- components: [container],
461
- ephemeral: true,
462
- flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
463
- });
464
- }
465
- }
466
-
467
- if (!interaction.logger) {
468
- interaction.logger = this.logger;
469
- }
470
- if (this.container && !this.container.logger) {
471
- this.container.logger = this.logger;
472
- }
473
- await command.execute(interaction, this.container);
474
- }
475
-
476
- /**
477
- * Handle AutoModeration action execution
478
- * @private
479
- */
480
- async _handleAutoModerationAction(execution) {
481
- const guildId = execution.guild.id;
482
- const ruleName = execution.ruleTriggerType.toString();
483
-
484
- const settings = await this.ServerSetting.getCache({ guildId: guildId });
485
- const locale = execution.guild.preferredLocale;
486
-
487
- if (!settings || !settings.modLogChannelId) {
488
- return;
489
- }
490
-
491
- const logChannelId = settings.modLogChannelId;
492
- const logChannel = await execution.guild.channels.fetch(logChannelId).catch(() => null);
493
-
494
- if (logChannel) {
495
- const embed = new EmbedBuilder()
496
- .setColor('Red')
497
- .setDescription(
498
- await this.t(
499
- null,
500
- 'common.automod',
501
- {
502
- ruleName: ruleName,
503
- },
504
- locale
505
- )
506
- )
507
- .addFields(
508
- {
509
- name: await this.t(null, 'common.automod.field.user', {}, locale),
510
- value: `${execution.user.tag} (${execution.userId})`,
511
- inline: true,
512
- },
513
- { name: await this.t(null, 'common.automod.field.rule.trigger', {}, locale), value: `\`${ruleName}\``, inline: true }
514
- )
515
- .setFooter({
516
- text: await this.t(
517
- null,
518
- 'common.embed.footer',
519
- {
520
- username: execution.guild.client.user.username,
521
- },
522
- locale
523
- ),
524
- })
525
- .setTimestamp();
526
-
527
- await logChannel.send({ embeds: [embed] });
528
- }
529
- }
530
-
531
- /**
532
- * Handle interaction errors
533
- * @private
534
- */
535
- async _handleInteractionError(interaction, error) {
536
- this.logger.error(`Error in interaction handler for ${interaction.user.tag}:`, error);
537
-
538
- if (this.kythiaConfig.sentry && this.kythiaConfig.sentry.dsn) {
539
- Sentry.withScope((scope) => {
540
- scope.setUser({ id: interaction.user.id, username: interaction.user.tag });
541
- scope.setTag('command', interaction.commandName);
542
- if (interaction.guild) {
543
- scope.setContext('guild', {
544
- id: interaction.guild.id,
545
- name: interaction.guild.name,
546
- });
547
- }
548
- Sentry.captureException(error);
549
- });
550
- }
551
-
552
- const ownerFirstId = this.kythiaConfig.owner.ids.split(',')[0].trim();
553
- const components = [
554
- new ContainerBuilder()
555
- .setAccentColor(convertColor('Red', { from: 'discord', to: 'decimal' }))
556
- .addTextDisplayComponents(new TextDisplayBuilder().setContent(await this.t(interaction, 'common.error.generic')))
557
- .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true))
558
- .addActionRowComponents(
559
- new ActionRowBuilder().addComponents(
560
- new ButtonBuilder()
561
- .setStyle(ButtonStyle.Link)
562
- .setLabel(await this.t(interaction, 'common.error.button.join.support.server'))
563
- .setURL(this.kythiaConfig.settings.supportServer),
564
- new ButtonBuilder()
565
- .setStyle(ButtonStyle.Link)
566
- .setLabel(await this.t(interaction, 'common.error.button.contact.owner'))
567
- .setURL(`discord://-/users/${ownerFirstId}`)
568
- )
569
- ),
570
- ];
571
- try {
572
- if (interaction.replied || interaction.deferred) {
573
- await interaction.followUp({
574
- components,
575
- flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
576
- ephemeral: true,
577
- });
578
- } else {
579
- await interaction.reply({
580
- components,
581
- flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
582
- ephemeral: true,
583
- });
584
- }
585
- } catch (e) {
586
- this.logger.error('Failed to send interaction error message:', e);
587
- }
588
-
589
- try {
590
- if (
591
- this.kythiaConfig.api &&
592
- this.kythiaConfig.api.webhookErrorLogs &&
593
- this.kythiaConfig.settings &&
594
- this.kythiaConfig.settings.webhookErrorLogs === true
595
- ) {
596
- const webhookClient = new WebhookClient({ url: this.kythiaConfig.api.webhookErrorLogs });
597
- const errorEmbed = new EmbedBuilder()
598
- .setColor('Red')
599
- .setDescription(`## ❌ Error at ${interaction.user.tag}\n` + `\`\`\`${error.stack}\`\`\``)
600
- .setFooter({ text: interaction.guild ? `Error from server ${interaction.guild.name}` : 'Error from DM' })
601
- .setTimestamp();
602
- await webhookClient.send({ embeds: [errorEmbed] });
603
- }
604
- } catch (webhookErr) {
605
- this.logger.error('Error sending interaction error webhook:', webhookErr);
606
- }
607
- }
32
+ /**
33
+ * 🏗️ InteractionManager Constructor
34
+ * @param {Object} client - Discord client instance
35
+ * @param {Object} container - Dependency container
36
+ * @param {Object} handlers - Handler maps from AddonManager
37
+ */
38
+ constructor({ client, container, handlers }) {
39
+ this.client = client;
40
+ this.container = container;
41
+ this.buttonHandlers = handlers.buttonHandlers;
42
+ this.modalHandlers = handlers.modalHandlers;
43
+ this.selectMenuHandlers = handlers.selectMenuHandlers;
44
+ this.autocompleteHandlers = handlers.autocompleteHandlers;
45
+ this.commandCategoryMap = handlers.commandCategoryMap;
46
+ this.categoryToFeatureMap = handlers.categoryToFeatureMap;
47
+
48
+ this.kythiaConfig = this.container.kythiaConfig;
49
+ this.models = this.container.models;
50
+ this.helpers = this.container.helpers;
51
+
52
+ this.logger = this.container.logger;
53
+ this.t = this.container.t;
54
+
55
+ this.ServerSetting = this.models.ServerSetting;
56
+ this.KythiaVoter = this.models.KythiaVoter;
57
+ this.isTeam = this.helpers.discord.isTeam;
58
+ this.isOwner = this.helpers.discord.isOwner;
59
+ }
60
+
61
+ /**
62
+ * 🛎️ Initialize Interaction Handler
63
+ * Sets up the main Discord interaction handler for commands, autocomplete, buttons, and modals.
64
+ */
65
+ initialize() {
66
+ function formatPerms(permsArray) {
67
+ return permsArray
68
+ .map((perm) => perm.replace(/([A-Z])/g, ' $1').trim())
69
+ .join(', ');
70
+ }
71
+
72
+ this.client.on(Events.InteractionCreate, async (interaction) => {
73
+ try {
74
+ if (interaction.isChatInputCommand()) {
75
+ await this._handleChatInputCommand(interaction, formatPerms);
76
+ } else if (interaction.isAutocomplete()) {
77
+ await this._handleAutocomplete(interaction);
78
+ } else if (interaction.isButton()) {
79
+ await this._handleButton(interaction);
80
+ } else if (interaction.isModalSubmit()) {
81
+ await this._handleModalSubmit(interaction);
82
+ } else if (interaction.isAnySelectMenu()) {
83
+ await this._handleSelectMenu(interaction);
84
+ } else if (
85
+ interaction.isUserContextMenuCommand() ||
86
+ interaction.isMessageContextMenuCommand()
87
+ ) {
88
+ await this._handleContextMenuCommand(interaction, formatPerms);
89
+ }
90
+ } catch (error) {
91
+ await this._handleInteractionError(interaction, error);
92
+ }
93
+ });
94
+
95
+ this.client.on(Events.AutoModerationActionExecution, async (execution) => {
96
+ try {
97
+ await this._handleAutoModerationAction(execution);
98
+ } catch (err) {
99
+ this.logger.error(
100
+ `[AutoMod Logger] Error during execution for ${execution.guild.name}:`,
101
+ err,
102
+ );
103
+ }
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Handle chat input commands
109
+ * @private
110
+ */
111
+ async _handleChatInputCommand(interaction, formatPerms) {
112
+ let commandKey = interaction.commandName;
113
+ const group = interaction.options.getSubcommandGroup(false);
114
+ const subcommand = interaction.options.getSubcommand(false);
115
+
116
+ if (group) commandKey = `${commandKey} ${group} ${subcommand}`;
117
+ else if (subcommand) commandKey = `${commandKey} ${subcommand}`;
118
+
119
+ let command = this.client.commands.get(commandKey);
120
+
121
+ if (!command && (subcommand || group)) {
122
+ command = this.client.commands.get(interaction.commandName);
123
+ }
124
+ if (!command) {
125
+ this.logger.error(`Command not found for key: ${commandKey}`);
126
+ return interaction.reply({
127
+ content: await this.t(interaction, 'common.error.command.not.found'),
128
+ flags: MessageFlags.Ephemeral,
129
+ });
130
+ }
131
+
132
+ if (interaction.inGuild()) {
133
+ const category = this.commandCategoryMap.get(interaction.commandName);
134
+ const featureFlag = this.categoryToFeatureMap.get(category);
135
+
136
+ if (featureFlag && !this.isOwner(interaction.user.id)) {
137
+ const settings = await this.ServerSetting.getCache({
138
+ guildId: interaction.guild.id,
139
+ });
140
+
141
+ if (
142
+ settings &&
143
+ Object.hasOwn(settings, featureFlag) &&
144
+ settings[featureFlag] === false
145
+ ) {
146
+ const featureName =
147
+ category.charAt(0).toUpperCase() + category.slice(1);
148
+ const reply = await this.t(
149
+ interaction,
150
+ 'common.error.feature.disabled',
151
+ { feature: featureName },
152
+ );
153
+ return interaction.reply({ content: reply });
154
+ }
155
+ }
156
+ }
157
+
158
+ if (command.guildOnly && !interaction.inGuild()) {
159
+ return interaction.reply({
160
+ content: await this.t(interaction, 'common.error.guild.only'),
161
+ flags: MessageFlags.Ephemeral,
162
+ });
163
+ }
164
+ if (command.ownerOnly && !this.isOwner(interaction.user.id)) {
165
+ return interaction.reply({
166
+ content: await this.t(interaction, 'common.error.not.owner'),
167
+ flags: MessageFlags.Ephemeral,
168
+ });
169
+ }
170
+ if (command.teamOnly && !this.isOwner(interaction.user.id)) {
171
+ const isTeamMember = await this.isTeam(interaction.user);
172
+ if (!isTeamMember)
173
+ return interaction.reply({
174
+ content: await this.t(interaction, 'common.error.not.team'),
175
+ flags: MessageFlags.Ephemeral,
176
+ });
177
+ }
178
+ if (command.permissions && interaction.inGuild()) {
179
+ const missingPerms = interaction.member.permissions.missing(
180
+ command.permissions,
181
+ );
182
+ if (missingPerms.length > 0)
183
+ return interaction.reply({
184
+ content: await this.t(
185
+ interaction,
186
+ 'common.error.user.missing.perms',
187
+ { perms: formatPerms(missingPerms) },
188
+ ),
189
+ flags: MessageFlags.Ephemeral,
190
+ });
191
+ }
192
+ if (command.botPermissions && interaction.inGuild()) {
193
+ const missingPerms = interaction.guild.members.me.permissions.missing(
194
+ command.botPermissions,
195
+ );
196
+ if (missingPerms.length > 0)
197
+ return interaction.reply({
198
+ content: await this.t(interaction, 'common.error.bot.missing.perms', {
199
+ perms: formatPerms(missingPerms),
200
+ }),
201
+ flags: MessageFlags.Ephemeral,
202
+ });
203
+ }
204
+
205
+ if (command.voteLocked && !this.isOwner(interaction.user.id)) {
206
+ const voter = await this.KythiaVoter.getCache({
207
+ userId: interaction.user.id,
208
+ });
209
+ const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000);
210
+
211
+ if (!voter || voter.votedAt < twelveHoursAgo) {
212
+ const container = new ContainerBuilder().setAccentColor(
213
+ convertColor(this.kythiaConfig.bot.color, {
214
+ from: 'hex',
215
+ to: 'decimal',
216
+ }),
217
+ );
218
+ container.addTextDisplayComponents(
219
+ new TextDisplayBuilder().setContent(
220
+ await this.t(interaction, 'common.error.vote.locked.text'),
221
+ ),
222
+ );
223
+ container.addSeparatorComponents(
224
+ new SeparatorBuilder()
225
+ .setSpacing(SeparatorSpacingSize.Small)
226
+ .setDivider(true),
227
+ );
228
+ container.addActionRowComponents(
229
+ new ActionRowBuilder().addComponents(
230
+ new ButtonBuilder()
231
+ .setLabel(
232
+ await this.t(interaction, 'common.error.vote.locked.button', {
233
+ username: interaction.client.user.username,
234
+ }),
235
+ )
236
+ .setStyle(ButtonStyle.Link)
237
+ .setURL(
238
+ `https://top.gg/bot/${this.kythiaConfig.bot.clientId}/vote`,
239
+ ),
240
+ ),
241
+ );
242
+ container.addSeparatorComponents(
243
+ new SeparatorBuilder()
244
+ .setSpacing(SeparatorSpacingSize.Small)
245
+ .setDivider(true),
246
+ );
247
+ container.addTextDisplayComponents(
248
+ new TextDisplayBuilder().setContent(
249
+ await this.t(interaction, 'common.container.footer', {
250
+ username: interaction.client.user.username,
251
+ }),
252
+ ),
253
+ );
254
+ return interaction.reply({
255
+ components: [container],
256
+ flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
257
+ });
258
+ }
259
+ }
260
+
261
+ const cooldownDuration =
262
+ command.cooldown ?? this.kythiaConfig.bot.globalCommandCooldown ?? 0;
263
+
264
+ if (cooldownDuration > 0 && !this.isOwner(interaction.user.id)) {
265
+ const { cooldowns } = this.client;
266
+
267
+ if (!cooldowns.has(command.name)) {
268
+ cooldowns.set(command.name, new Collection());
269
+ }
270
+
271
+ const now = Date.now();
272
+ const timestamps = cooldowns.get(command.name);
273
+ const cooldownAmount = cooldownDuration * 1000;
274
+
275
+ if (timestamps.has(interaction.user.id)) {
276
+ const expirationTime =
277
+ timestamps.get(interaction.user.id) + cooldownAmount;
278
+
279
+ if (now < expirationTime) {
280
+ const timeLeft = (expirationTime - now) / 1000;
281
+ const reply = await this.t(interaction, 'common.error.cooldown', {
282
+ time: timeLeft.toFixed(1),
283
+ });
284
+ return interaction.reply({
285
+ content: reply,
286
+ flags: MessageFlags.Ephemeral,
287
+ });
288
+ }
289
+ }
290
+
291
+ timestamps.set(interaction.user.id, now);
292
+ setTimeout(() => timestamps.delete(interaction.user.id), cooldownAmount);
293
+ }
294
+
295
+ if (typeof command.execute === 'function') {
296
+ if (!interaction.logger) {
297
+ interaction.logger = this.logger;
298
+ }
299
+
300
+ if (this.container && !this.container.logger) {
301
+ this.container.logger = this.logger;
302
+ }
303
+ if (command.execute.length === 2) {
304
+ await command.execute(interaction, this.container);
305
+ } else {
306
+ await command.execute(interaction);
307
+ }
308
+ } else {
309
+ this.logger.error(
310
+ "Command doesn't have a valid 'execute' function:",
311
+ command.name || commandKey,
312
+ );
313
+ return interaction.reply({
314
+ content: await this.t(
315
+ interaction,
316
+ 'common.error.command.execution.invalid',
317
+ ),
318
+ flags: MessageFlags.Ephemeral,
319
+ });
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Handle autocomplete interactions
325
+ * @private
326
+ */
327
+ async _handleAutocomplete(interaction) {
328
+ let commandKey = interaction.commandName;
329
+ const group = interaction.options.getSubcommandGroup(false);
330
+ const subcommand = interaction.options.getSubcommand(false);
331
+
332
+ if (group) commandKey = `${commandKey} ${group} ${subcommand}`;
333
+ else if (subcommand) commandKey = `${commandKey} ${subcommand}`;
334
+
335
+ let handler = this.autocompleteHandlers.get(commandKey);
336
+
337
+ if (!handler && (subcommand || group)) {
338
+ handler = this.autocompleteHandlers.get(interaction.commandName);
339
+ }
340
+
341
+ if (handler) {
342
+ try {
343
+ await handler(interaction, this.container);
344
+ } catch (err) {
345
+ this.logger.error(
346
+ `Error in autocomplete handler for ${commandKey}:`,
347
+ err,
348
+ );
349
+ try {
350
+ await interaction.respond([]);
351
+ } catch (e) {
352
+ this.logger.error(e);
353
+ }
354
+ }
355
+ } else {
356
+ try {
357
+ await interaction.respond([]);
358
+ } catch (e) {
359
+ this.logger.error(e);
360
+ }
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Handle button interactions
366
+ * @private
367
+ */
368
+ async _handleButton(interaction) {
369
+ const customIdPrefix = interaction.customId.includes('|')
370
+ ? interaction.customId.split('|')[0]
371
+ : interaction.customId.split(':')[0];
372
+
373
+ const handler = this.buttonHandlers.get(customIdPrefix);
374
+
375
+ if (handler) {
376
+ if (
377
+ typeof handler === 'object' &&
378
+ typeof handler.execute === 'function'
379
+ ) {
380
+ await handler.execute(interaction, this.container);
381
+ } else if (typeof handler === 'function') {
382
+ if (handler.length === 2) {
383
+ await handler(interaction, this.container);
384
+ } else {
385
+ await handler(interaction);
386
+ }
387
+ } else {
388
+ this.logger.error(
389
+ `Handler for button ${customIdPrefix} has an invalid format`,
390
+ );
391
+ }
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Handle modal submit interactions
397
+ * @private
398
+ */
399
+ async _handleModalSubmit(interaction) {
400
+ const customIdPrefix = interaction.customId.includes('|')
401
+ ? interaction.customId.split('|')[0]
402
+ : interaction.customId.split(':')[0];
403
+
404
+ this.logger.info(
405
+ `Modal submit - customId: ${interaction.customId}, prefix: ${customIdPrefix}`,
406
+ );
407
+
408
+ const handler = this.modalHandlers.get(customIdPrefix);
409
+ this.logger.info(`Modal handler found: ${!!handler}`);
410
+
411
+ if (handler) {
412
+ if (
413
+ typeof handler === 'object' &&
414
+ typeof handler.execute === 'function'
415
+ ) {
416
+ await handler.execute(interaction, this.container);
417
+ } else if (typeof handler === 'function') {
418
+ if (handler.length === 2) {
419
+ await handler(interaction, this.container);
420
+ } else {
421
+ await handler(interaction);
422
+ }
423
+ } else {
424
+ this.logger.error(
425
+ `Handler untuk modal ${customIdPrefix} formatnya salah (bukan fungsi atau { execute: ... })`,
426
+ );
427
+ }
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Handle select menu interactions
433
+ * @private
434
+ */
435
+ async _handleSelectMenu(interaction) {
436
+ const customIdPrefix = interaction.customId.includes('|')
437
+ ? interaction.customId.split('|')[0]
438
+ : interaction.customId.split(':')[0];
439
+
440
+ this.logger.info(
441
+ `Select menu submit - customId: ${interaction.customId}, prefix: ${customIdPrefix}`,
442
+ );
443
+
444
+ const handler = this.selectMenuHandlers.get(customIdPrefix);
445
+ this.logger.info(`Select menu handler found: ${!!handler}`);
446
+
447
+ if (handler) {
448
+ if (
449
+ typeof handler === 'object' &&
450
+ typeof handler.execute === 'function'
451
+ ) {
452
+ await handler.execute(interaction, this.container);
453
+ } else if (typeof handler === 'function') {
454
+ if (handler.length === 2) {
455
+ await handler(interaction, this.container);
456
+ } else {
457
+ await handler(interaction);
458
+ }
459
+ } else {
460
+ this.logger.error(
461
+ `Handler untuk select menu ${customIdPrefix} formatnya salah`,
462
+ );
463
+ }
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Handle context menu commands
469
+ * @private
470
+ */
471
+ async _handleContextMenuCommand(interaction, formatPerms) {
472
+ const command = this.client.commands.get(interaction.commandName);
473
+ if (!command) return;
474
+
475
+ if (command.guildOnly && !interaction.inGuild()) {
476
+ return interaction.reply({
477
+ content: await this.t(interaction, 'common.error.guild.only'),
478
+ flags: MessageFlags.Ephemeral,
479
+ });
480
+ }
481
+ if (command.ownerOnly && !this.isOwner(interaction.user.id)) {
482
+ return interaction.reply({
483
+ content: await this.t(interaction, 'common.error.not.owner'),
484
+ flags: MessageFlags.Ephemeral,
485
+ });
486
+ }
487
+ if (command.teamOnly && !this.isOwner(interaction.user.id)) {
488
+ const isTeamMember = await this.isTeam(interaction.user);
489
+ if (!isTeamMember)
490
+ return interaction.reply({
491
+ content: await this.t(interaction, 'common.error.not.team'),
492
+ flags: MessageFlags.Ephemeral,
493
+ });
494
+ }
495
+ if (command.permissions && interaction.inGuild()) {
496
+ const missingPerms = interaction.member.permissions.missing(
497
+ command.permissions,
498
+ );
499
+ if (missingPerms.length > 0)
500
+ return interaction.reply({
501
+ content: await this.t(
502
+ interaction,
503
+ 'common.error.user.missing.perms',
504
+ { perms: formatPerms(missingPerms) },
505
+ ),
506
+ flags: MessageFlags.Ephemeral,
507
+ });
508
+ }
509
+ if (command.botPermissions && interaction.inGuild()) {
510
+ const missingPerms = interaction.guild.members.me.permissions.missing(
511
+ command.botPermissions,
512
+ );
513
+ if (missingPerms.length > 0)
514
+ return interaction.reply({
515
+ content: await this.t(interaction, 'common.error.bot.missing.perms', {
516
+ perms: formatPerms(missingPerms),
517
+ }),
518
+ flags: MessageFlags.Ephemeral,
519
+ });
520
+ }
521
+ if (command.isInMainGuild && !this.isOwner(interaction.user.id)) {
522
+ const mainGuild = this.client.guilds.cache.get(
523
+ this.kythiaConfig.bot.mainGuildId,
524
+ );
525
+ if (!mainGuild) {
526
+ this.logger.error(
527
+ `❌ [isInMainGuild Check] Error: Bot is not a member of the main guild specified in config: ${this.kythiaConfig.bot.mainGuildId}`,
528
+ );
529
+ }
530
+ try {
531
+ await mainGuild.members.fetch(interaction.user.id);
532
+ } catch (error) {
533
+ const container = new ContainerBuilder().setAccentColor(
534
+ convertColor(this.kythiaConfig.bot.color, {
535
+ from: 'hex',
536
+ to: 'decimal',
537
+ }),
538
+ );
539
+ container.addTextDisplayComponents(
540
+ new TextDisplayBuilder().setContent(
541
+ await this.t(interaction, 'common.error.not.in.main.guild.text', {
542
+ name: mainGuild.name,
543
+ }),
544
+ ),
545
+ );
546
+ container.addSeparatorComponents(
547
+ new SeparatorBuilder()
548
+ .setSpacing(SeparatorSpacingSize.Small)
549
+ .setDivider(true),
550
+ );
551
+ container.addActionRowComponents(
552
+ new ActionRowBuilder().addComponents(
553
+ new ButtonBuilder()
554
+ .setLabel(
555
+ await this.t(
556
+ interaction,
557
+ 'common.error.not.in.main.guild.button.join',
558
+ ),
559
+ )
560
+ .setStyle(ButtonStyle.Link)
561
+ .setURL(this.kythiaConfig.settings.supportServer),
562
+ ),
563
+ );
564
+ container.addTextDisplayComponents(
565
+ new TextDisplayBuilder().setContent(
566
+ await this.t(interaction, 'common.container.footer', {
567
+ username: interaction.client.user.username,
568
+ }),
569
+ ),
570
+ );
571
+ this.logger.error(error);
572
+ return interaction.reply({
573
+ components: [container],
574
+ flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
575
+ });
576
+ }
577
+ }
578
+ if (command.voteLocked && !this.isOwner(interaction.user.id)) {
579
+ const voter = await this.KythiaVoter.getCache({
580
+ userId: interaction.user.id,
581
+ });
582
+ const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000);
583
+
584
+ if (!voter || voter.votedAt < twelveHoursAgo) {
585
+ const container = new ContainerBuilder().setAccentColor(
586
+ convertColor(this.kythiaConfig.bot.color, {
587
+ from: 'hex',
588
+ to: 'decimal',
589
+ }),
590
+ );
591
+ container.addTextDisplayComponents(
592
+ new TextDisplayBuilder().setContent(
593
+ await this.t(interaction, 'common.error.vote.locked.text'),
594
+ ),
595
+ );
596
+ container.addSeparatorComponents(
597
+ new SeparatorBuilder()
598
+ .setSpacing(SeparatorSpacingSize.Small)
599
+ .setDivider(true),
600
+ );
601
+ container.addActionRowComponents(
602
+ new ActionRowBuilder().addComponents(
603
+ new ButtonBuilder()
604
+ .setLabel(
605
+ await this.t(interaction, 'common.error.vote.locked.button', {
606
+ username: interaction.client.user.username,
607
+ }),
608
+ )
609
+ .setStyle(ButtonStyle.Link)
610
+ .setURL(
611
+ `https://top.gg/bot/${this.kythiaConfig.bot.clientId}/vote`,
612
+ ),
613
+ ),
614
+ );
615
+ container.addSeparatorComponents(
616
+ new SeparatorBuilder()
617
+ .setSpacing(SeparatorSpacingSize.Small)
618
+ .setDivider(true),
619
+ );
620
+ container.addTextDisplayComponents(
621
+ new TextDisplayBuilder().setContent(
622
+ await this.t(interaction, 'common.container.footer', {
623
+ username: interaction.client.user.username,
624
+ }),
625
+ ),
626
+ );
627
+ return interaction.reply({
628
+ components: [container],
629
+ flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
630
+ });
631
+ }
632
+ }
633
+
634
+ if (!interaction.logger) {
635
+ interaction.logger = this.logger;
636
+ }
637
+ if (this.container && !this.container.logger) {
638
+ this.container.logger = this.logger;
639
+ }
640
+ await command.execute(interaction, this.container);
641
+ }
642
+
643
+ /**
644
+ * Handle AutoModeration action execution
645
+ * @private
646
+ */
647
+ async _handleAutoModerationAction(execution) {
648
+ const guildId = execution.guild.id;
649
+ const ruleName = execution.ruleTriggerType.toString();
650
+
651
+ const settings = await this.ServerSetting.getCache({ guildId: guildId });
652
+ const locale = execution.guild.preferredLocale;
653
+
654
+ if (!settings || !settings.modLogChannelId) {
655
+ return;
656
+ }
657
+
658
+ const logChannelId = settings.modLogChannelId;
659
+ const logChannel = await execution.guild.channels
660
+ .fetch(logChannelId)
661
+ .catch(() => null);
662
+
663
+ if (logChannel) {
664
+ const embed = new EmbedBuilder()
665
+ .setColor('Red')
666
+ .setDescription(
667
+ await this.t(
668
+ null,
669
+ 'common.automod',
670
+ {
671
+ ruleName: ruleName,
672
+ },
673
+ locale,
674
+ ),
675
+ )
676
+ .addFields(
677
+ {
678
+ name: await this.t(null, 'common.automod.field.user', {}, locale),
679
+ value: `${execution.user.tag} (${execution.userId})`,
680
+ inline: true,
681
+ },
682
+ {
683
+ name: await this.t(
684
+ null,
685
+ 'common.automod.field.rule.trigger',
686
+ {},
687
+ locale,
688
+ ),
689
+ value: `\`${ruleName}\``,
690
+ inline: true,
691
+ },
692
+ )
693
+ .setFooter({
694
+ text: await this.t(
695
+ null,
696
+ 'common.embed.footer',
697
+ {
698
+ username: execution.guild.client.user.username,
699
+ },
700
+ locale,
701
+ ),
702
+ })
703
+ .setTimestamp();
704
+
705
+ await logChannel.send({ embeds: [embed] });
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Handle interaction errors
711
+ * @private
712
+ */
713
+ async _handleInteractionError(interaction, error) {
714
+ this.logger.error(
715
+ `Error in interaction handler for ${interaction.user.tag}:`,
716
+ error,
717
+ );
718
+
719
+ if (this.kythiaConfig.sentry?.dsn) {
720
+ Sentry.withScope((scope) => {
721
+ scope.setUser({
722
+ id: interaction.user.id,
723
+ username: interaction.user.tag,
724
+ });
725
+ scope.setTag('command', interaction.commandName);
726
+ if (interaction.guild) {
727
+ scope.setContext('guild', {
728
+ id: interaction.guild.id,
729
+ name: interaction.guild.name,
730
+ });
731
+ }
732
+ Sentry.captureException(error);
733
+ });
734
+ }
735
+
736
+ const ownerFirstId = this.kythiaConfig.owner.ids.split(',')[0].trim();
737
+ const components = [
738
+ new ContainerBuilder()
739
+ .setAccentColor(convertColor('Red', { from: 'discord', to: 'decimal' }))
740
+ .addTextDisplayComponents(
741
+ new TextDisplayBuilder().setContent(
742
+ await this.t(interaction, 'common.error.generic'),
743
+ ),
744
+ )
745
+ .addSeparatorComponents(
746
+ new SeparatorBuilder()
747
+ .setSpacing(SeparatorSpacingSize.Small)
748
+ .setDivider(true),
749
+ )
750
+ .addActionRowComponents(
751
+ new ActionRowBuilder().addComponents(
752
+ new ButtonBuilder()
753
+ .setStyle(ButtonStyle.Link)
754
+ .setLabel(
755
+ await this.t(
756
+ interaction,
757
+ 'common.error.button.join.support.server',
758
+ ),
759
+ )
760
+ .setURL(this.kythiaConfig.settings.supportServer),
761
+ new ButtonBuilder()
762
+ .setStyle(ButtonStyle.Link)
763
+ .setLabel(
764
+ await this.t(interaction, 'common.error.button.contact.owner'),
765
+ )
766
+ .setURL(`https://discord.com/users/${ownerFirstId}`),
767
+ ),
768
+ ),
769
+ ];
770
+ try {
771
+ if (interaction.replied || interaction.deferred) {
772
+ await interaction.followUp({
773
+ components,
774
+ flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
775
+ });
776
+ } else {
777
+ await interaction.reply({
778
+ components,
779
+ flags: MessageFlags.Ephemeral | MessageFlags.IsComponentsV2,
780
+ });
781
+ }
782
+ } catch (e) {
783
+ this.logger.error('Failed to send interaction error message:', e);
784
+ }
785
+
786
+ try {
787
+ if (
788
+ this.kythiaConfig.api?.webhookErrorLogs &&
789
+ this.kythiaConfig.settings &&
790
+ this.kythiaConfig.settings.webhookErrorLogs === true
791
+ ) {
792
+ const webhookClient = new WebhookClient({
793
+ url: this.kythiaConfig.api.webhookErrorLogs,
794
+ });
795
+ const errorEmbed = new EmbedBuilder()
796
+ .setColor('Red')
797
+ .setDescription(
798
+ `## ❌ Error at ${interaction.user.tag}\n` +
799
+ `\`\`\`${error.stack}\`\`\``,
800
+ )
801
+ .setFooter({
802
+ text: interaction.guild
803
+ ? `Error from server ${interaction.guild.name}`
804
+ : 'Error from DM',
805
+ })
806
+ .setTimestamp();
807
+ await webhookClient.send({ embeds: [errorEmbed] });
808
+ }
809
+ } catch (webhookErr) {
810
+ this.logger.error('Error sending interaction error webhook:', webhookErr);
811
+ }
812
+ }
608
813
  }
609
814
 
610
815
  module.exports = InteractionManager;