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,99 @@
1
+ /**
2
+ * 🔔 Event Manager
3
+ *
4
+ * @file src/managers/EventManager.js
5
+ * @copyright © 2025 kenndeclouv
6
+ * @assistant chaa & graa
7
+ * @version 0.9.3-beta
8
+ *
9
+ * @description
10
+ * Handles all Discord event listeners except InteractionCreate.
11
+ * Manages event registration, execution order, and error handling for all events.
12
+ */
13
+
14
+ class EventManager {
15
+ /**
16
+ * 🏗️ EventManager Constructor
17
+ * @param {Object} client - Discord client instance
18
+ * @param {Object} container - Dependency container
19
+ * @param {Map} eventHandlers - Event handlers map from AddonManager
20
+ */
21
+ constructor({ client, container, eventHandlers }) {
22
+ this.client = client;
23
+ this.container = container;
24
+ this.eventHandlers = eventHandlers;
25
+
26
+ this.logger = this.container.logger;
27
+ }
28
+
29
+ /**
30
+ * 🚦 Initialize Master Event Handlers
31
+ * Creates a single listener for each event type that then executes all
32
+ * registered addon handlers in their prioritized order.
33
+ */
34
+ initialize() {
35
+ for (const [eventName, handlers] of this.eventHandlers.entries()) {
36
+ this.client.on(eventName, async (...args) => {
37
+ for (const handler of handlers) {
38
+ try {
39
+ const stopPropagation = await handler(this, ...args);
40
+
41
+ if (stopPropagation === true) {
42
+ break;
43
+ }
44
+ } catch (error) {
45
+ this.logger.error(`Error executing event handler for [${eventName}]:`, error);
46
+ }
47
+ }
48
+ });
49
+ }
50
+
51
+ this.logger.info(`✅ EventManager initialized with ${this.eventHandlers.size} event types`);
52
+ }
53
+
54
+ /**
55
+ * Add a new event handler
56
+ * @param {string} eventName - Name of the event
57
+ * @param {Function} handler - Handler function
58
+ */
59
+ addEventHandler(eventName, handler) {
60
+ if (!this.eventHandlers.has(eventName)) {
61
+ this.eventHandlers.set(eventName, []);
62
+ }
63
+ this.eventHandlers.get(eventName).push(handler);
64
+ }
65
+
66
+ /**
67
+ * Remove an event handler
68
+ * @param {string} eventName - Name of the event
69
+ * @param {Function} handler - Handler function to remove
70
+ */
71
+ removeEventHandler(eventName, handler) {
72
+ if (this.eventHandlers.has(eventName)) {
73
+ const handlers = this.eventHandlers.get(eventName);
74
+ const index = handlers.indexOf(handler);
75
+ if (index > -1) {
76
+ handlers.splice(index, 1);
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get all handlers for a specific event
83
+ * @param {string} eventName - Name of the event
84
+ * @returns {Array} Array of handlers
85
+ */
86
+ getEventHandlers(eventName) {
87
+ return this.eventHandlers.get(eventName) || [];
88
+ }
89
+
90
+ /**
91
+ * Get all registered event types
92
+ * @returns {Array} Array of event names
93
+ */
94
+ getEventTypes() {
95
+ return Array.from(this.eventHandlers.keys());
96
+ }
97
+ }
98
+
99
+ module.exports = EventManager;
@@ -0,0 +1,553 @@
1
+ /**
2
+ * 🎯 Interaction Manager
3
+ *
4
+ * @file src/managers/InteractionManager.js
5
+ * @copyright © 2025 kenndeclouv
6
+ * @assistant chaa & graa
7
+ * @version 0.9.3-beta
8
+ *
9
+ * @description
10
+ * Handles all Discord interaction events including slash commands, buttons, modals,
11
+ * autocomplete, and context menu commands. Manages permissions, cooldowns, and error handling.
12
+ */
13
+
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,
27
+ } = require('discord.js');
28
+ const convertColor = require('../utils/color');
29
+ const Sentry = require('@sentry/node');
30
+
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.isUserContextMenuCommand() || interaction.isMessageContextMenuCommand()) {
81
+ await this._handleContextMenuCommand(interaction, formatPerms);
82
+ }
83
+ } catch (error) {
84
+ await this._handleInteractionError(interaction, error);
85
+ }
86
+ });
87
+
88
+ this.client.on(Events.AutoModerationActionExecution, async (execution) => {
89
+ try {
90
+ await this._handleAutoModerationAction(execution);
91
+ } catch (err) {
92
+ this.logger.error(`[AutoMod Logger] Error during execution for ${execution.guild.name}:`, err);
93
+ }
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Handle chat input commands
99
+ * @private
100
+ */
101
+ async _handleChatInputCommand(interaction, formatPerms) {
102
+ let commandKey = interaction.commandName;
103
+ const group = interaction.options.getSubcommandGroup(false);
104
+ const subcommand = interaction.options.getSubcommand(false);
105
+
106
+ if (group) commandKey = `${commandKey} ${group} ${subcommand}`;
107
+ else if (subcommand) commandKey = `${commandKey} ${subcommand}`;
108
+
109
+ let command = this.client.commands.get(commandKey);
110
+
111
+ if (!command && (subcommand || group)) {
112
+ command = this.client.commands.get(interaction.commandName);
113
+ }
114
+ if (!command) {
115
+ this.logger.error(`Command not found for key: ${commandKey}`);
116
+ return interaction.reply({ content: await this.t(interaction, 'common.error.command.not.found'), ephemeral: true });
117
+ }
118
+
119
+ if (interaction.inGuild()) {
120
+ const category = this.commandCategoryMap.get(interaction.commandName);
121
+ const featureFlag = this.categoryToFeatureMap.get(category);
122
+
123
+ if (featureFlag && !this.isOwner(interaction.user.id)) {
124
+ const settings = await this.ServerSetting.getCache({ guildId: interaction.guild.id });
125
+
126
+ if (settings && Object.prototype.hasOwnProperty.call(settings, featureFlag) && settings[featureFlag] === false) {
127
+ const featureName = category.charAt(0).toUpperCase() + category.slice(1);
128
+ const reply = await this.t(interaction, 'common.error.feature.disabled', { feature: featureName });
129
+ return interaction.reply({ content: reply });
130
+ }
131
+ }
132
+ }
133
+
134
+ if (command.guildOnly && !interaction.inGuild()) {
135
+ return interaction.reply({ content: await this.t(interaction, 'common.error.guild.only'), ephemeral: true });
136
+ }
137
+ if (command.ownerOnly && !this.isOwner(interaction.user.id)) {
138
+ return interaction.reply({ content: await this.t(interaction, 'common.error.not.owner'), ephemeral: true });
139
+ }
140
+ if (command.teamOnly && !this.isOwner(interaction.user.id)) {
141
+ const isTeamMember = await this.isTeam(interaction.user);
142
+ if (!isTeamMember) return interaction.reply({ content: await this.t(interaction, 'common.error.not.team'), ephemeral: true });
143
+ }
144
+ if (command.permissions && interaction.inGuild()) {
145
+ const missingPerms = interaction.member.permissions.missing(command.permissions);
146
+ if (missingPerms.length > 0)
147
+ return interaction.reply({
148
+ content: await this.t(interaction, 'common.error.user.missing.perms', { perms: formatPerms(missingPerms) }),
149
+ ephemeral: true,
150
+ });
151
+ }
152
+ if (command.botPermissions && interaction.inGuild()) {
153
+ const missingPerms = interaction.guild.members.me.permissions.missing(command.botPermissions);
154
+ if (missingPerms.length > 0)
155
+ return interaction.reply({
156
+ content: await this.t(interaction, 'common.error.bot.missing.perms', { perms: formatPerms(missingPerms) }),
157
+ ephemeral: true,
158
+ });
159
+ }
160
+
161
+ if (command.voteLocked && !this.isOwner(interaction.user.id)) {
162
+ const voter = await this.KythiaVoter.getCache({ userId: interaction.user.id });
163
+ const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000);
164
+
165
+ if (!voter || voter.votedAt < twelveHoursAgo) {
166
+ const container = new ContainerBuilder().setAccentColor(
167
+ convertColor(this.kythiaConfig.bot.color, { from: 'hex', to: 'decimal' })
168
+ );
169
+ container.addTextDisplayComponents(
170
+ new TextDisplayBuilder().setContent(await this.t(interaction, 'common.error.vote.locked.text'))
171
+ );
172
+ container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
173
+ container.addActionRowComponents(
174
+ new ActionRowBuilder().addComponents(
175
+ new ButtonBuilder()
176
+ .setLabel(
177
+ await this.t(interaction, 'common.error.vote.locked.button', {
178
+ botName: interaction.client.user.username,
179
+ })
180
+ )
181
+ .setStyle(ButtonStyle.Link)
182
+ .setURL(`https://top.gg/bot/${this.kythiaConfig.bot.clientId}/vote`)
183
+ )
184
+ );
185
+ container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
186
+ container.addTextDisplayComponents(
187
+ new TextDisplayBuilder().setContent(await this.t(interaction, 'common.container.footer'))
188
+ );
189
+ return interaction.reply({
190
+ components: [container],
191
+ ephemeral: true,
192
+ flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
193
+ });
194
+ }
195
+ }
196
+
197
+ const cooldownDuration = command.cooldown ?? this.kythiaConfig.bot.globalCommandCooldown ?? 0;
198
+
199
+ if (cooldownDuration > 0 && !this.isOwner(interaction.user.id)) {
200
+ const { cooldowns } = this.client;
201
+
202
+ if (!cooldowns.has(command.name)) {
203
+ cooldowns.set(command.name, new Collection());
204
+ }
205
+
206
+ const now = Date.now();
207
+ const timestamps = cooldowns.get(command.name);
208
+ const cooldownAmount = cooldownDuration * 1000;
209
+
210
+ if (timestamps.has(interaction.user.id)) {
211
+ const expirationTime = timestamps.get(interaction.user.id) + cooldownAmount;
212
+
213
+ if (now < expirationTime) {
214
+ const timeLeft = (expirationTime - now) / 1000;
215
+ const reply = await this.t(interaction, 'common.error.cooldown', { time: timeLeft.toFixed(1) });
216
+ return interaction.reply({ content: reply, ephemeral: true });
217
+ }
218
+ }
219
+
220
+ timestamps.set(interaction.user.id, now);
221
+ setTimeout(() => timestamps.delete(interaction.user.id), cooldownAmount);
222
+ }
223
+
224
+ if (typeof command.execute === 'function') {
225
+ // Ensure logger is defined for the command execution context
226
+ if (!interaction.logger) {
227
+ interaction.logger = this.logger;
228
+ }
229
+ // Also inject logger into the container for legacy/compatibility
230
+ if (this.container && !this.container.logger) {
231
+ this.container.logger = this.logger;
232
+ }
233
+ if (command.execute.length === 2) {
234
+ await command.execute(interaction, this.container);
235
+ } else {
236
+ await command.execute(interaction);
237
+ }
238
+ } else {
239
+ this.logger.error("Command doesn't have a valid 'execute' function:", command.name || commandKey);
240
+ return interaction.reply({ content: await this.t(interaction, 'common.error.command.execution.invalid'), ephemeral: true });
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Handle autocomplete interactions
246
+ * @private
247
+ */
248
+ async _handleAutocomplete(interaction) {
249
+ let commandKey = interaction.commandName;
250
+ const group = interaction.options.getSubcommandGroup(false);
251
+ const subcommand = interaction.options.getSubcommand(false);
252
+
253
+ if (group) commandKey = `${commandKey} ${group} ${subcommand}`;
254
+ else if (subcommand) commandKey = `${commandKey} ${subcommand}`;
255
+
256
+ let handler = this.autocompleteHandlers.get(commandKey);
257
+
258
+ if (!handler && (subcommand || group)) {
259
+ handler = this.autocompleteHandlers.get(interaction.commandName);
260
+ }
261
+
262
+ if (handler) {
263
+ try {
264
+ await handler(interaction, this.container);
265
+ } catch (err) {
266
+ this.logger.error(`Error in autocomplete handler for ${commandKey}:`, err);
267
+ try {
268
+ await interaction.respond([]);
269
+ } catch (e) {}
270
+ }
271
+ } else {
272
+ try {
273
+ await interaction.respond([]);
274
+ } catch (e) {}
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Handle button interactions
280
+ * @private
281
+ */
282
+ async _handleButton(interaction) {
283
+ const handler = this.buttonHandlers.get(interaction.customId.split('_')[0]);
284
+ if (handler) await handler(interaction, this.container);
285
+ }
286
+
287
+ /**
288
+ * Handle modal submit interactions
289
+ * @private
290
+ */
291
+ async _handleModalSubmit(interaction) {
292
+ const customIdPrefix = interaction.customId.includes('|') ? interaction.customId.split('|')[0] : interaction.customId.split(':')[0];
293
+ this.logger.info('Modal submit - customId:', interaction.customId, 'prefix:', customIdPrefix);
294
+ const handler = this.modalHandlers.get(customIdPrefix);
295
+ this.logger.info('Modal handler found:', !!handler);
296
+ if (handler) await handler(interaction, this.container);
297
+ }
298
+
299
+ /**
300
+ * Handle context menu commands
301
+ * @private
302
+ */
303
+ async _handleContextMenuCommand(interaction, formatPerms) {
304
+ const command = this.client.commands.get(interaction.commandName);
305
+ if (!command) return;
306
+
307
+ if (command.guildOnly && !interaction.inGuild()) {
308
+ return interaction.reply({ content: await this.t(interaction, 'common.error.guild.only'), ephemeral: true });
309
+ }
310
+ if (command.ownerOnly && !this.isOwner(interaction.user.id)) {
311
+ return interaction.reply({ content: await this.t(interaction, 'common.error.not.owner'), ephemeral: true });
312
+ }
313
+ if (command.teamOnly && !this.isOwner(interaction.user.id)) {
314
+ const isTeamMember = await this.isTeam(interaction.user);
315
+ if (!isTeamMember) return interaction.reply({ content: await this.t(interaction, 'common.error.not.team'), ephemeral: true });
316
+ }
317
+ if (command.permissions && interaction.inGuild()) {
318
+ const missingPerms = interaction.member.permissions.missing(command.permissions);
319
+ if (missingPerms.length > 0)
320
+ return interaction.reply({
321
+ content: await this.t(interaction, 'common.error.user.missing.perms', { perms: formatPerms(missingPerms) }),
322
+ ephemeral: true,
323
+ });
324
+ }
325
+ if (command.botPermissions && interaction.inGuild()) {
326
+ const missingPerms = interaction.guild.members.me.permissions.missing(command.botPermissions);
327
+ if (missingPerms.length > 0)
328
+ return interaction.reply({
329
+ content: await this.t(interaction, 'common.error.bot.missing.perms', { perms: formatPerms(missingPerms) }),
330
+ ephemeral: true,
331
+ });
332
+ }
333
+ if (command.isInMainGuild && !this.isOwner(interaction.user.id)) {
334
+ const mainGuild = this.client.guilds.cache.get(this.kythiaConfig.bot.mainGuildId);
335
+ if (!mainGuild) {
336
+ this.logger.error(
337
+ `❌ [isInMainGuild Check] Error: Bot is not a member of the main guild specified in config: ${this.kythiaConfig.bot.mainGuildId}`
338
+ );
339
+ }
340
+ try {
341
+ await mainGuild.members.fetch(interaction.user.id);
342
+ } catch (error) {
343
+ const container = new ContainerBuilder().setAccentColor(
344
+ convertColor(this.kythiaConfig.bot.color, { from: 'hex', to: 'decimal' })
345
+ );
346
+ container.addTextDisplayComponents(
347
+ new TextDisplayBuilder().setContent(
348
+ await this.t(interaction, 'common.error.not.in.main.guild.text', { name: mainGuild.name })
349
+ )
350
+ );
351
+ container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
352
+ container.addActionRowComponents(
353
+ new ActionRowBuilder().addComponents(
354
+ new ButtonBuilder()
355
+ .setLabel(await this.t(interaction, 'common.error.not.in.main.guild.button.join'))
356
+ .setStyle(ButtonStyle.Link)
357
+ .setURL(this.kythiaConfig.settings.supportServer)
358
+ )
359
+ );
360
+ container.addTextDisplayComponents(
361
+ new TextDisplayBuilder().setContent(
362
+ await this.t(interaction, 'common.container.footer', { username: interaction.client.user.username })
363
+ )
364
+ );
365
+ return interaction.reply({
366
+ components: [container],
367
+ flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
368
+ });
369
+ }
370
+ }
371
+ if (command.voteLocked && !this.isOwner(interaction.user.id)) {
372
+ const voter = await this.KythiaVoter.getCache({ userId: interaction.user.id });
373
+ const twelveHoursAgo = new Date(Date.now() - 12 * 60 * 60 * 1000);
374
+
375
+ if (!voter || voter.votedAt < twelveHoursAgo) {
376
+ const container = new ContainerBuilder().setAccentColor(
377
+ convertColor(this.kythiaConfig.bot.color, { from: 'hex', to: 'decimal' })
378
+ );
379
+ container.addTextDisplayComponents(
380
+ new TextDisplayBuilder().setContent(await this.t(interaction, 'common.error.vote.locked.text'))
381
+ );
382
+ container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
383
+ container.addActionRowComponents(
384
+ new ActionRowBuilder().addComponents(
385
+ new ButtonBuilder()
386
+ .setLabel(
387
+ await this.t(interaction, 'common.error.vote.locked.button', {
388
+ username: interaction.client.user.username,
389
+ })
390
+ )
391
+ .setStyle(ButtonStyle.Link)
392
+ .setURL(`https://top.gg/bot/${this.kythiaConfig.bot.clientId}/vote`)
393
+ )
394
+ );
395
+ container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true));
396
+ container.addTextDisplayComponents(
397
+ new TextDisplayBuilder().setContent(
398
+ await this.t(interaction, 'common.container.footer', { username: interaction.client.user.username })
399
+ )
400
+ );
401
+ return interaction.reply({
402
+ components: [container],
403
+ ephemeral: true,
404
+ flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
405
+ });
406
+ }
407
+ }
408
+
409
+ // Ensure logger is available in the execution context
410
+ if (!interaction.logger) {
411
+ interaction.logger = this.logger;
412
+ }
413
+ if (this.container && !this.container.logger) {
414
+ this.container.logger = this.logger;
415
+ }
416
+ await command.execute(interaction, this.container);
417
+ }
418
+
419
+ /**
420
+ * Handle AutoModeration action execution
421
+ * @private
422
+ */
423
+ async _handleAutoModerationAction(execution) {
424
+ const guildId = execution.guild.id;
425
+ const ruleName = execution.ruleTriggerType.toString();
426
+
427
+ const settings = await this.ServerSetting.getCache({ guildId: guildId });
428
+ const locale = execution.guild.preferredLocale;
429
+
430
+ if (!settings || !settings.modLogChannelId) {
431
+ return;
432
+ }
433
+
434
+ const logChannelId = settings.modLogChannelId;
435
+ const logChannel = await execution.guild.channels.fetch(logChannelId).catch(() => null);
436
+
437
+ if (logChannel) {
438
+ const embed = new EmbedBuilder()
439
+ .setColor('Red')
440
+ .setDescription(
441
+ await this.t(
442
+ null,
443
+ 'common.automod',
444
+ {
445
+ ruleName: ruleName,
446
+ },
447
+ locale
448
+ )
449
+ )
450
+ .addFields(
451
+ {
452
+ name: await this.t(null, 'common.automod.field.user', {}, locale),
453
+ value: `${execution.user.tag} (${execution.userId})`,
454
+ inline: true,
455
+ },
456
+ { name: await this.t(null, 'common.automod.field.rule.trigger', {}, locale), value: `\`${ruleName}\``, inline: true }
457
+ )
458
+ .setFooter({
459
+ text: await this.t(
460
+ null,
461
+ 'common.embed.footer',
462
+ {
463
+ username: execution.guild.client.user.username,
464
+ },
465
+ locale
466
+ ),
467
+ })
468
+ .setTimestamp();
469
+
470
+ await logChannel.send({ embeds: [embed] });
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Handle interaction errors
476
+ * @private
477
+ */
478
+ async _handleInteractionError(interaction, error) {
479
+ this.logger.error(`Error in interaction handler for ${interaction.user.tag}:`, error);
480
+
481
+ if (this.kythiaConfig.sentry && this.kythiaConfig.sentry.dsn) {
482
+ Sentry.withScope((scope) => {
483
+ scope.setUser({ id: interaction.user.id, username: interaction.user.tag });
484
+ scope.setTag('command', interaction.commandName);
485
+ if (interaction.guild) {
486
+ scope.setContext('guild', {
487
+ id: interaction.guild.id,
488
+ name: interaction.guild.name,
489
+ });
490
+ }
491
+ Sentry.captureException(error);
492
+ });
493
+ }
494
+
495
+ const ownerFirstId = this.kythiaConfig.owner.ids.split(',')[0].trim();
496
+ const components = [
497
+ new ContainerBuilder()
498
+ .setAccentColor(convertColor('Red', { from: 'discord', to: 'decimal' }))
499
+ .addTextDisplayComponents(new TextDisplayBuilder().setContent(await this.t(interaction, 'common.error.generic')))
500
+ .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small).setDivider(true))
501
+ .addActionRowComponents(
502
+ new ActionRowBuilder().addComponents(
503
+ new ButtonBuilder()
504
+ .setStyle(ButtonStyle.Link)
505
+ .setLabel(await this.t(interaction, 'common.error.button.join.support.server'))
506
+ .setURL(this.kythiaConfig.settings.supportServer),
507
+ new ButtonBuilder()
508
+ .setStyle(ButtonStyle.Link)
509
+ .setLabel(await this.t(interaction, 'common.error.button.contact.owner'))
510
+ .setURL(`discord://-/users/${ownerFirstId}`)
511
+ )
512
+ ),
513
+ ];
514
+ try {
515
+ if (interaction.replied || interaction.deferred) {
516
+ await interaction.followUp({
517
+ components,
518
+ flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
519
+ ephemeral: true,
520
+ });
521
+ } else {
522
+ await interaction.reply({
523
+ components,
524
+ flags: MessageFlags.IsPersistent | MessageFlags.IsComponentsV2,
525
+ ephemeral: true,
526
+ });
527
+ }
528
+ } catch (e) {
529
+ this.logger.error('Failed to send interaction error message:', e);
530
+ }
531
+
532
+ try {
533
+ if (
534
+ this.kythiaConfig.api &&
535
+ this.kythiaConfig.api.webhookErrorLogs &&
536
+ this.kythiaConfig.settings &&
537
+ this.kythiaConfig.settings.webhookErrorLogs === true
538
+ ) {
539
+ const webhookClient = new WebhookClient({ url: this.kythiaConfig.api.webhookErrorLogs });
540
+ const errorEmbed = new EmbedBuilder()
541
+ .setColor('Red')
542
+ .setDescription(`## ❌ Error at ${interaction.user.tag}\n` + `\`\`\`${error.stack}\`\`\``)
543
+ .setFooter({ text: interaction.guild ? `Error from server ${interaction.guild.name}` : 'Error from DM' })
544
+ .setTimestamp();
545
+ await webhookClient.send({ embeds: [errorEmbed] });
546
+ }
547
+ } catch (webhookErr) {
548
+ this.logger.error('Error sending interaction error webhook:', webhookErr);
549
+ }
550
+ }
551
+ }
552
+
553
+ module.exports = InteractionManager;