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