Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
1import { browserHeaders } from '@/constants/index';
2import BotClient from '@/services/Client';
3import { RandomReddit } from '@/types/base';
4import { RemindCommandProps } from '@/types/command';
5import logger from '@/utils/logger';
6import { sanitizeInput, getUnallowedWordCategory } from '@/utils/validation';
7import { isUserBanned, incrementUserStrike } from '@/utils/userStrikes';
8import {
9 ButtonStyle,
10 ClientEvents,
11 ContainerBuilder,
12 MessageFlags,
13 MediaGalleryBuilder,
14 MediaGalleryItemBuilder,
15 ActionRowBuilder,
16 ButtonBuilder,
17 TextDisplayBuilder,
18 SeparatorBuilder,
19 SeparatorSpacingSize,
20 ButtonInteraction,
21 type MessageActionRowComponentBuilder,
22} from 'discord.js';
23
24type InteractionHandler = (...args: ClientEvents['interactionCreate']) => void;
25
26export default class InteractionCreateEvent {
27 private client: BotClient;
28 constructor(c: BotClient) {
29 this.client = c;
30 c.on('interactionCreate', this.handleInteraction.bind(this));
31 }
32
33 private handleInteraction: InteractionHandler = async (i) => {
34 if (i.isAutocomplete()) {
35 const command = this.client.commands.get(i.commandName);
36 if (command && typeof command.autocomplete === 'function') {
37 await command.autocomplete(this.client, i);
38 return;
39 }
40 }
41 if (i.isChatInputCommand()) {
42 const userId = i.user.id;
43 const bannedUntil = await isUserBanned(userId);
44 if (bannedUntil) {
45 return i.reply({
46 content: `You are banned from using Aethel commands until <t:${Math.floor(bannedUntil.getTime() / 1000)}:F>.`,
47 flags: MessageFlags.Ephemeral,
48 });
49 }
50 const options = i.options.data;
51 for (const opt of options) {
52 if (typeof opt.value === 'string') {
53 const category = getUnallowedWordCategory(opt.value);
54 if (category) {
55 const { strike_count, banned_until } = await incrementUserStrike(userId);
56 if (banned_until && new Date(banned_until) > new Date()) {
57 return i.reply({
58 content: `You have been banned from using Aethel commands for 7 days due to repeated use of unallowed language. Ban expires: <t:${Math.floor(new Date(banned_until).getTime() / 1000)}:F>.`,
59 flags: MessageFlags.Ephemeral,
60 });
61 }
62 return i.reply({
63 content: `Your request was flagged by Aethel for ${category}. You have ${strike_count}/5 strikes. For more information, visit https://aethel.xyz/legal/terms`,
64 flags: MessageFlags.Ephemeral,
65 });
66 }
67 }
68 }
69 const command = this.client.commands.get(i.commandName);
70 if (!command) {
71 return i.reply({
72 content: 'Command not found',
73 flags: MessageFlags.Ephemeral,
74 });
75 }
76 try {
77 command.execute(this.client, i);
78 } catch (error) {
79 console.error(`[COMMAND ERROR] ${i.commandName}:`, error);
80 await i.reply({
81 content: 'There was an error executing this command!',
82 flags: MessageFlags.Ephemeral,
83 });
84 }
85 }
86 if (i.isModalSubmit()) {
87 if (i.customId.startsWith('remind')) {
88 const remind = this.client.commands.get('remind') as RemindCommandProps;
89 if (remind && remind.handleModal) {
90 await remind.handleModal(this.client, i);
91 }
92 } else if (i.customId === 'apiCredentials') {
93 const ai = this.client.commands.get('ai');
94 if (ai && 'handleModal' in ai) {
95 await (ai as unknown as RemindCommandProps).handleModal(this.client, i);
96 }
97 }
98 return;
99 }
100 if (i.isMessageContextMenuCommand()) {
101 let targetCommand = null;
102 for (const [, command] of this.client.commands) {
103 if ('contextMenuExecute' in command) {
104 const remindCommand = command as RemindCommandProps;
105 if (remindCommand.contextMenu.name === i.commandName) {
106 targetCommand = command;
107 break;
108 }
109 }
110 }
111
112 if (!targetCommand) {
113 await i.reply({ content: 'Error Occured, Please try again later' });
114 return;
115 }
116
117 (targetCommand as RemindCommandProps).contextMenuExecute(this.client, i);
118 }
119 if (i.isButton()) {
120 try {
121 if (i.customId.startsWith('trivia_')) {
122 const triviaCommand = this.client.commands.get('trivia');
123 if (triviaCommand && 'handleButton' in triviaCommand) {
124 return await (
125 triviaCommand as {
126 handleButton: (client: BotClient, interaction: ButtonInteraction) => Promise<void>;
127 }
128 ).handleButton(this.client, i);
129 }
130 }
131
132 const originalUser = i.message.interaction!.user;
133 if (originalUser.id !== i.user.id) {
134 return await i.reply({
135 content: 'Only the person who used the command can refresh the image!',
136 flags: MessageFlags.Ephemeral,
137 });
138 }
139
140 if (i.customId === 'refresh_cat') {
141 try {
142 const response = await fetch('https://api.pur.cat/random-cat');
143 if (!response.ok) {
144 return await i.update({
145 content: await this.client.getLocaleText('commands.cat.error', i.locale),
146 components: [],
147 });
148 }
149 const data = (await response.json()) as RandomReddit;
150 if (data.url) {
151 const title = data.title
152 ? sanitizeInput(data.title).slice(0, 245) + '...'
153 : await this.client.getLocaleText('random.cat', i.locale);
154
155 const refreshLabel = await this.client.getLocaleText('commands.cat.newcat', i.locale);
156
157 const container = new ContainerBuilder()
158 .setAccentColor(0xfaa0a0)
159 .addTextDisplayComponents(new TextDisplayBuilder().setContent(`# ${title}`))
160 .addTextDisplayComponents(
161 new TextDisplayBuilder().setContent(
162 data.subreddit
163 ? await this.client.getLocaleText('reddit.from', i.locale, {
164 subreddit: data.subreddit,
165 })
166 : '',
167 ),
168 )
169 .addMediaGalleryComponents(
170 new MediaGalleryBuilder().addItems(
171 new MediaGalleryItemBuilder().setURL(data.url),
172 ),
173 )
174 .addActionRowComponents(
175 new ActionRowBuilder<ButtonBuilder>().addComponents(
176 new ButtonBuilder()
177 .setStyle(ButtonStyle.Danger)
178 .setLabel(refreshLabel)
179 .setEmoji({ name: '🐱' })
180 .setCustomId('refresh_cat'),
181 ),
182 );
183
184 await i.update({
185 components: [container],
186 flags: MessageFlags.IsComponentsV2,
187 });
188 } else {
189 const container = new ContainerBuilder().addTextDisplayComponents(
190 new TextDisplayBuilder().setContent(
191 await this.client.getLocaleText('commands.cat.error', i.locale),
192 ),
193 );
194
195 await i.update({
196 components: [container],
197 flags: MessageFlags.IsComponentsV2,
198 });
199 }
200 } catch (error) {
201 logger.error('Error refreshing cat image:', error);
202 const container = new ContainerBuilder().addTextDisplayComponents(
203 new TextDisplayBuilder().setContent(
204 await this.client.getLocaleText('commands.cat.error', i.locale),
205 ),
206 );
207
208 await i.update({
209 components: [container],
210 flags: MessageFlags.IsComponentsV2,
211 });
212 }
213 } else if (i.customId.startsWith('help_commands_')) {
214 const customIdParts = i.customId.split('_');
215 const originalUserId = customIdParts[2];
216
217 if (originalUserId !== i.user.id) {
218 return await i.reply({
219 content: 'Only the person who used the command can view commands!',
220 flags: MessageFlags.Ephemeral,
221 });
222 }
223
224 const commandCategories: Map<string, string[]> = new Map();
225
226 for (const cmd of this.client.commands.values()) {
227 const ClientApplicationCommandCache = this.client.application?.commands.cache.find(
228 (command) => command.name === cmd.data.name,
229 );
230 const category = cmd.category || 'Uncategorized';
231 if (!commandCategories.has(category)) {
232 commandCategories.set(category, []);
233 }
234
235 const localizedDescription = await this.client.getLocaleText(
236 `commands.${cmd.data.name}.description`,
237 i.locale,
238 );
239 commandCategories
240 .get(category)!
241 .push(
242 `</${ClientApplicationCommandCache?.name}:${ClientApplicationCommandCache?.id}> - ${localizedDescription}`,
243 );
244 }
245
246 const container = new ContainerBuilder()
247 .setAccentColor(0x5865f2)
248 .addTextDisplayComponents(
249 new TextDisplayBuilder().setContent('# 📋 **Available Commands**'),
250 );
251
252 for (const [category, cmds] of commandCategories.entries()) {
253 const localizedCategory = await this.client.getLocaleText(
254 `categories.${category}`,
255 i.locale,
256 );
257
258 container.addTextDisplayComponents(
259 new TextDisplayBuilder().setContent(`\n## 📂 ${localizedCategory}`),
260 );
261
262 container.addTextDisplayComponents(
263 new TextDisplayBuilder().setContent(
264 cmds.map((line) => line.replace(/\u007F/g, '')).join('\n'),
265 ),
266 );
267 }
268
269 const backLabel =
270 (await this.client.getLocaleText('commands.help.back', i.locale)) || 'Back';
271 container.addActionRowComponents(
272 new ActionRowBuilder<ButtonBuilder>().addComponents(
273 new ButtonBuilder()
274 .setStyle(ButtonStyle.Secondary)
275 .setLabel(backLabel)
276 .setEmoji({ name: '⬅️' })
277 .setCustomId(`help_back_${i.user.id}`),
278 ),
279 );
280
281 await i.update({
282 components: [container],
283 flags: MessageFlags.IsComponentsV2,
284 });
285 } else if (i.customId.startsWith('help_back_')) {
286 const customIdParts = i.customId.split('_');
287 const originalUserId = customIdParts[2];
288
289 if (originalUserId !== i.user.id) {
290 return await i.reply({
291 content: 'Only the person who used the command can go back!',
292 flags: MessageFlags.Ephemeral,
293 });
294 }
295
296 const [
297 title,
298 description,
299 viewCommandsText,
300 supportServerText,
301 linksSocialText,
302 featuresText,
303 featuresContent,
304 dashboardText,
305 ] = await Promise.all([
306 this.client.getLocaleText('commands.help.title', i.locale),
307 this.client.getLocaleText('commands.help.about', i.locale),
308 this.client.getLocaleText('commands.help.viewcommands', i.locale),
309 this.client.getLocaleText('commands.help.supportserver', i.locale),
310 this.client.getLocaleText('commands.help.links_social', i.locale),
311 this.client.getLocaleText('commands.help.features', i.locale),
312 this.client.getLocaleText('commands.help.features_content', i.locale),
313 this.client.getLocaleText('commands.help.dashboard', i.locale),
314 ]);
315
316 const container = new ContainerBuilder()
317 .setAccentColor(0xf4f4f4)
318
319 .addMediaGalleryComponents(
320 new MediaGalleryBuilder().addItems(
321 new MediaGalleryItemBuilder().setURL('https://aethel.xyz/aethel_banner_white.png'),
322 ),
323 )
324 .addTextDisplayComponents(
325 new TextDisplayBuilder().setContent(`# ${title || 'Aethel Bot'}`),
326 )
327 .addTextDisplayComponents(
328 new TextDisplayBuilder().setContent(description || 'Get information about Aethel'),
329 )
330 .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
331 .addTextDisplayComponents(
332 new TextDisplayBuilder().setContent(
333 `\n## **${linksSocialText || 'Links & Social Media'}**`,
334 ),
335 )
336 .addTextDisplayComponents(
337 new TextDisplayBuilder().setContent(
338 '[Website](https://aethel.xyz) • [GitHub](https://github.com/aethel-labs/aethel) • [Bluesky](https://bsky.app/profile/aethel.xyz)',
339 ),
340 )
341 .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
342 .addTextDisplayComponents(
343 new TextDisplayBuilder().setContent(`\n## **${featuresText || 'Features'}**`),
344 )
345 .addTextDisplayComponents(
346 new TextDisplayBuilder().setContent(
347 featuresContent ||
348 '**Fun Commands** - 8ball, cat/dog images, and more\n' +
349 '**AI Integration** - Powered by OpenAI and other providers\n' +
350 '**Reminders** - Never forget important tasks\n' +
351 '**Utilities** - Weather, help, and productivity tools\n' +
352 '**Multi-language** - Supports multiple languages',
353 ),
354 )
355
356 .addSeparatorComponents(
357 new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setDivider(true),
358 )
359 .addTextDisplayComponents(
360 new TextDisplayBuilder().setContent(
361 `-# ${dashboardText || 'Dashboard available at https://aethel.xyz/login for To-Dos, Reminders and custom AI API key management'}`,
362 ),
363 )
364 .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large))
365 .addActionRowComponents(
366 new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents(
367 new ButtonBuilder()
368 .setStyle(ButtonStyle.Primary)
369 .setLabel(viewCommandsText || 'Commands')
370 .setCustomId(`help_commands_${i.user.id}`),
371 new ButtonBuilder()
372 .setStyle(ButtonStyle.Link)
373 .setLabel(supportServerText || 'Support')
374 .setURL('https://discord.gg/63stE8pEaK'),
375 ),
376 );
377
378 await i.update({
379 components: [container],
380 flags: MessageFlags.IsComponentsV2,
381 });
382 } else if (i.customId === 'refresh_dog') {
383 try {
384 const response = await fetch('https://api.erm.dog/random-dog', {
385 headers: browserHeaders,
386 });
387 if (!response.ok) {
388 const container = new ContainerBuilder().addTextDisplayComponents(
389 new TextDisplayBuilder().setContent(
390 await this.client.getLocaleText('commands.dog.error', i.locale),
391 ),
392 );
393
394 return await i.update({
395 components: [container],
396 flags: MessageFlags.IsComponentsV2,
397 });
398 }
399 let data;
400 let isJson = true;
401 let url = null;
402 try {
403 data = (await response.json()) as RandomReddit;
404 } catch {
405 isJson = false;
406 }
407 if (isJson && data!.url) {
408 url = data!.url;
409 } else {
410 const response2 = await fetch('https://api.erm.dog/random-dog', {
411 headers: browserHeaders,
412 });
413 url = await response2.text();
414 data = { url };
415 }
416 if (url && url.startsWith('http')) {
417 const title = data!.title
418 ? sanitizeInput(data!.title).slice(0, 245) + '...'
419 : await this.client.getLocaleText('commands.dog.randomdog', i.locale);
420
421 const refreshLabel = await this.client.getLocaleText('commands.dog.newdog', i.locale);
422
423 const container = new ContainerBuilder()
424 .setAccentColor(0x8a2be2)
425 .addTextDisplayComponents(new TextDisplayBuilder().setContent(`# ${title}`))
426 .addTextDisplayComponents(
427 new TextDisplayBuilder().setContent(
428 data!.subreddit
429 ? await this.client.getLocaleText('reddit.from', i.locale, {
430 subreddit: data!.subreddit,
431 })
432 : '',
433 ),
434 )
435 .addMediaGalleryComponents(
436 new MediaGalleryBuilder().addItems(
437 new MediaGalleryItemBuilder().setURL(data!.url),
438 ),
439 )
440 .addActionRowComponents(
441 new ActionRowBuilder<ButtonBuilder>().addComponents(
442 new ButtonBuilder()
443 .setStyle(ButtonStyle.Secondary)
444 .setLabel(refreshLabel)
445 .setEmoji({ name: '🐶' })
446 .setCustomId('refresh_dog'),
447 ),
448 );
449
450 await i.update({
451 components: [container],
452 flags: MessageFlags.IsComponentsV2,
453 });
454 } else {
455 const container = new ContainerBuilder().addTextDisplayComponents(
456 new TextDisplayBuilder().setContent(
457 await this.client.getLocaleText('commands.dog.error', i.locale),
458 ),
459 );
460
461 await i.update({
462 components: [container],
463 flags: MessageFlags.IsComponentsV2,
464 });
465 }
466 } catch (error) {
467 logger.error('Error refreshing dog image:', error);
468 const container = new ContainerBuilder().addTextDisplayComponents(
469 new TextDisplayBuilder().setContent(
470 await this.client.getLocaleText('commands.dog.error', i.locale),
471 ),
472 );
473
474 await i.update({
475 components: [container],
476 flags: MessageFlags.IsComponentsV2,
477 });
478 }
479 }
480 } catch (error) {
481 logger.error('Unexpected error in button interaction:', error);
482 await i.update({
483 content: await this.client.getLocaleText('unexpectederror', i.locale),
484 components: [],
485 });
486 }
487 }
488 };
489}