kythia-core 0.9.3-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +409 -0
- package/README.md +307 -0
- package/index.js +17 -0
- package/package.json +46 -0
- package/src/Kythia.js +430 -0
- package/src/KythiaClient.js +53 -0
- package/src/database/KythiaModel.js +948 -0
- package/src/database/KythiaORM.js +481 -0
- package/src/database/KythiaSequelize.js +94 -0
- package/src/managers/AddonManager.js +954 -0
- package/src/managers/EventManager.js +99 -0
- package/src/managers/InteractionManager.js +553 -0
- package/src/managers/ShutdownManager.js +197 -0
- package/src/structures/BaseCommand.js +49 -0
- package/src/utils/color.js +176 -0
- package/src/utils/formatter.js +99 -0
- package/src/utils/index.js +4 -0
|
@@ -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;
|