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.

at main 536 lines 21 kB view raw
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}