Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
at dev 19 kB view raw
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}