Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
at main 24 kB view raw
1import { 2 ChatInputCommandInteraction, 3 InteractionReplyOptions, 4 MessagePayload, 5 EmbedBuilder, 6} from 'discord.js'; 7import { SlashCommandProps } from '@/types/command'; 8import BotClient from '@/services/Client'; 9import logger from '@/utils/logger'; 10 11export interface ToolCall { 12 name: string; 13 args: Record<string, unknown>; 14} 15 16type EmbedInput = Record<string, unknown> & { 17 data?: Record<string, unknown>; 18 toJSON?: () => unknown; 19 title?: unknown; 20 description?: unknown; 21 fields?: unknown; 22 color?: unknown; 23 timestamp?: unknown; 24 footer?: unknown; 25}; 26 27function toPlainEmbedObject(embed: unknown): Record<string, unknown> | unknown { 28 if (embed && typeof embed === 'object') { 29 const embedObj = embed as EmbedInput; 30 if ('data' in embedObj && embedObj.data) { 31 return embedObj.data as Record<string, unknown>; 32 } 33 if (typeof embedObj.toJSON === 'function') { 34 return embedObj.toJSON(); 35 } 36 const e = embedObj as Record<string, unknown>; 37 if ('title' in e || 'fields' in e || 'description' in e) { 38 return e; 39 } 40 return { 41 title: embedObj.title, 42 description: embedObj.description, 43 fields: embedObj.fields, 44 color: embedObj.color, 45 timestamp: embedObj.timestamp, 46 footer: embedObj.footer, 47 } as Record<string, unknown>; 48 } 49 return embed as Record<string, unknown>; 50} 51 52function testEmbedBuilderStructure() { 53 const testEmbed = new EmbedBuilder() 54 .setTitle('Test Title') 55 .setDescription('Test Description') 56 .addFields({ name: 'Test Field', value: 'Test Value', inline: true }); 57 58 logger.debug(`[Command Executor] Test EmbedBuilder structure:`, { 59 embed: testEmbed, 60 hasData: 'data' in testEmbed, 61 data: testEmbed.data, 62 hasToJSON: typeof testEmbed.toJSON === 'function', 63 toJSON: testEmbed.toJSON ? testEmbed.toJSON() : 'N/A', 64 keys: Object.keys(testEmbed), 65 prototype: Object.getPrototypeOf(testEmbed)?.constructor?.name, 66 }); 67 68 if (testEmbed.data) { 69 logger.debug(`[Command Executor] Test embed data fields:`, { 70 fields: testEmbed.data.fields, 71 fieldsLength: testEmbed.data.fields?.length, 72 allDataKeys: Object.keys(testEmbed.data), 73 }); 74 } 75} 76 77export function extractToolCalls(content: string): { cleanContent: string; toolCalls: ToolCall[] } { 78 const toolCallRegex = /{([^{}\s:]+):({[^{}]*}|[^{}]*)?}/g; 79 const toolCalls: ToolCall[] = []; 80 let cleanContent = content; 81 let match; 82 83 while ((match = toolCallRegex.exec(content)) !== null) { 84 try { 85 if (!match[1]) { 86 continue; 87 } 88 89 const toolName = match[1].trim(); 90 const argsString = match[2] ? match[2].trim() : ''; 91 92 if (!toolName) { 93 continue; 94 } 95 96 let args: Record<string, unknown> = {}; 97 98 if (argsString.startsWith('{') && argsString.endsWith('}')) { 99 try { 100 args = JSON.parse(argsString); 101 } catch (_error) { 102 args = { query: argsString }; 103 } 104 } else if (argsString) { 105 if (argsString.startsWith('"') && argsString.endsWith('"')) { 106 const unquoted = argsString.slice(1, -1); 107 if (toolName === 'reaction') { 108 args = { emoji: unquoted }; 109 } else { 110 args = { query: unquoted }; 111 } 112 } else { 113 args = { query: argsString }; 114 } 115 } else { 116 args = {}; 117 } 118 119 toolCalls.push({ 120 name: toolName, 121 args, 122 }); 123 124 cleanContent = cleanContent.replace(match[0], '').trim(); 125 } catch (error) { 126 logger.error(`Error parsing tool call: ${error}`); 127 } 128 } 129 130 return { cleanContent, toolCalls }; 131} 132 133export async function executeToolCall( 134 toolCall: ToolCall, 135 interaction: ChatInputCommandInteraction, 136 client: BotClient, 137): Promise<string> { 138 let { name, args } = toolCall; 139 140 if (name.includes(':')) { 141 const parts = name.split(':'); 142 name = parts[0]; 143 if (!args || Object.keys(args).length === 0) { 144 args = { search: parts[1] }; 145 } 146 } 147 148 try { 149 const validCommands = ['cat', 'dog', 'joke', '8ball', 'weather', 'wiki']; 150 151 if (!validCommands.includes(name.toLowerCase())) { 152 throw new Error( 153 `Command '${name}' is not a valid command. Available commands: ${validCommands.join(', ')}`, 154 ); 155 } 156 157 if (['cat', 'dog'].includes(name)) { 158 const commandName = name.charAt(0).toUpperCase() + name.slice(1); 159 logger.debug(`[${commandName}] Starting ${name} command execution`); 160 161 try { 162 const commandDir = 'fun'; 163 const commandModule = await import(`../commands/${commandDir}/${name}`); 164 165 const imageData = 166 name === 'cat' 167 ? await commandModule.fetchCatImage() 168 : await commandModule.fetchDogImage(); 169 170 return JSON.stringify({ 171 success: true, 172 type: name, 173 title: imageData.title || `Random ${commandName}`, 174 url: imageData.url, 175 subreddit: imageData.subreddit, 176 source: name === 'cat' ? 'pur.cat' : 'erm.dog', 177 handled: true, 178 }); 179 } catch (error) { 180 const errorMessage = 181 error instanceof Error ? error.message : `Unknown error in ${name} command`; 182 logger.error(`[${commandName}] Error: ${errorMessage}`, { error }); 183 return JSON.stringify({ 184 success: false, 185 error: `Failed to execute ${name} command: ${errorMessage}`, 186 handled: false, 187 }); 188 } 189 } 190 191 if (name === 'wiki') { 192 logger.debug('[Wiki] Starting wiki command execution'); 193 194 try { 195 const wikiModule = await import('../commands/utilities/wiki'); 196 197 let searchQuery = ''; 198 if (typeof args === 'object' && args !== null) { 199 const argsObj = args as Record<string, unknown>; 200 searchQuery = (argsObj.search as string) || (argsObj.query as string) || ''; 201 } 202 203 if (!searchQuery) { 204 return JSON.stringify({ 205 error: true, 206 message: 'Missing search query', 207 status: 400, 208 }); 209 } 210 211 try { 212 logger.debug( 213 `[Wiki] Searching Wikipedia for: ${searchQuery} (locale: ${interaction.locale || 'en'})`, 214 ); 215 216 const searchResult = await wikiModule.searchWikipedia( 217 searchQuery, 218 interaction.locale || 'en', 219 ); 220 logger.debug(`[Wiki] Search result:`, { 221 pageid: searchResult.pageid, 222 title: searchResult.title, 223 }); 224 225 const article = await wikiModule.getArticleSummary( 226 searchResult.pageid, 227 searchResult.wikiLang, 228 ); 229 logger.debug(`[Wiki] Retrieved article:`, { 230 title: article.title, 231 extractLength: article.extract?.length, 232 }); 233 234 const maxLength = 1500; 235 const truncated = article.extract && article.extract.length > maxLength; 236 const extract = truncated 237 ? article.extract.substring(0, maxLength) 238 : article.extract || 'No summary available for this article.'; 239 240 const response = { 241 title: article.title, 242 content: extract, 243 url: `https://${searchResult.wikiLang}.wikipedia.org/wiki/${encodeURIComponent(article.title.replace(/ /g, '_'))}`, 244 truncated: truncated, 245 }; 246 247 return JSON.stringify(response); 248 } catch (error) { 249 const errorMessage = error instanceof Error ? error.message : String(error); 250 const stack = error instanceof Error ? error.stack : undefined; 251 const responseStatus = 252 error instanceof Error && 'response' in error 253 ? (error as { response?: { status?: number } }).response?.status 254 : undefined; 255 256 logger.error('[Wiki] Error executing wiki command:', { 257 error: errorMessage, 258 stack, 259 responseStatus, 260 searchQuery, 261 locale: interaction.locale, 262 }); 263 264 return JSON.stringify({ 265 error: true, 266 message: 267 responseStatus === 404 268 ? 'No Wikipedia article found for that search query. Please try a different search term.' 269 : `Error searching Wikipedia: ${errorMessage}`, 270 status: responseStatus || 500, 271 }); 272 } 273 } catch (error) { 274 const errorMessage = error instanceof Error ? error.message : String(error); 275 const stack = error instanceof Error ? error.stack : undefined; 276 logger.error('[Wiki] Error in wiki command execution:', { error: errorMessage, stack }); 277 278 return JSON.stringify({ 279 error: true, 280 message: `Error in wiki command execution: ${errorMessage}`, 281 status: 500, 282 }); 283 } 284 } 285 286 let commandModule; 287 const isDev = process.env.NODE_ENV !== 'production'; 288 const ext = isDev ? '.ts' : '.js'; 289 290 try { 291 let commandDir = 'fun'; 292 if ( 293 ['weather', 'wiki', 'ai', 'cobalt', 'remind', 'social', 'time', 'todo', 'whois'].includes( 294 name, 295 ) 296 ) { 297 commandDir = 'utilities'; 298 } 299 300 const commandPath = `../commands/${commandDir}/${name}${ext}`; 301 logger.debug(`[Command Executor] Trying to import command from: ${commandPath}`); 302 commandModule = await import(commandPath).catch((e) => { 303 logger.error(`[Command Executor] Error importing command '${name}':`, e); 304 throw e; 305 }); 306 307 if (name === 'cat' || name === 'dog') { 308 try { 309 const imageData = 310 name === 'cat' 311 ? await commandModule.fetchCatImage() 312 : await commandModule.fetchDogImage(); 313 314 return JSON.stringify({ 315 success: true, 316 type: name, 317 title: imageData.title || `Random ${name === 'cat' ? 'Cat' : 'Dog'}`, 318 url: imageData.url, 319 subreddit: imageData.subreddit, 320 source: name === 'cat' ? 'pur.cat' : 'erm.dog', 321 }); 322 } catch (error) { 323 const errorMessage = 324 error instanceof Error ? error.message : `Unknown error fetching ${name} image`; 325 logger.error(`[${name}] Error: ${errorMessage}`, { error }); 326 return JSON.stringify({ 327 success: false, 328 error: `Failed to fetch ${name} image: ${errorMessage}`, 329 }); 330 } 331 } 332 } catch (error) { 333 logger.error(`[Command Executor] Error importing command '${name}':`, error); 334 throw new Error(`Command '${name}' not found`); 335 } 336 337 const command = commandModule.default as SlashCommandProps; 338 339 if (!command) { 340 throw new Error(`Command '${name}' not found`); 341 } 342 343 let capturedResponse: unknown = null; 344 345 const mockInteraction = { 346 ...interaction, 347 options: { 348 getString: (param: string) => { 349 if (typeof args === 'object' && args !== null) { 350 const argsObj = args as Record<string, unknown>; 351 return (argsObj[param] as string) || ''; 352 } 353 return ''; 354 }, 355 getNumber: (param: string) => { 356 if (typeof args === 'object' && args !== null) { 357 const argsObj = args as Record<string, unknown>; 358 const value = argsObj[param]; 359 return value !== null && value !== undefined ? Number(value) : null; 360 } 361 return null; 362 }, 363 getBoolean: (param: string) => { 364 if (typeof args === 'object' && args !== null) { 365 const argsObj = args as Record<string, unknown>; 366 const value = argsObj[param]; 367 return typeof value === 'boolean' ? value : null; 368 } 369 return null; 370 }, 371 }, 372 deferReply: async () => { 373 return Promise.resolve(); 374 }, 375 reply: async (options: InteractionReplyOptions | MessagePayload) => { 376 if ('embeds' in options && options.embeds && options.embeds.length > 0) { 377 const processedEmbeds = options.embeds.map((e) => toPlainEmbedObject(e)); 378 capturedResponse = { ...(options as Record<string, unknown>), embeds: processedEmbeds }; 379 } else { 380 capturedResponse = options; 381 } 382 if ('embeds' in options && options.embeds && options.embeds.length > 0) { 383 return JSON.stringify({ 384 success: true, 385 embeds: 386 'embeds' in (capturedResponse as Record<string, unknown>) 387 ? (capturedResponse as { embeds?: unknown[] }).embeds 388 : options.embeds, 389 }); 390 } 391 return JSON.stringify({ 392 success: true, 393 content: 'content' in options ? options.content : undefined, 394 }); 395 }, 396 editReply: async (options: InteractionReplyOptions | MessagePayload) => { 397 if ('embeds' in options && options.embeds && options.embeds.length > 0) { 398 const processedEmbeds = options.embeds.map((e) => toPlainEmbedObject(e)); 399 capturedResponse = { ...(options as Record<string, unknown>), embeds: processedEmbeds }; 400 } else { 401 capturedResponse = options; 402 } 403 logger.debug(`[Command Executor] editReply called with options:`, { 404 hasEmbeds: 'embeds' in options && options.embeds && options.embeds.length > 0, 405 hasContent: 'content' in options, 406 options: options, 407 }); 408 409 if ('embeds' in options && options.embeds && options.embeds.length > 0) { 410 logger.debug(`[Command Executor] Raw embeds before processing:`, options.embeds); 411 412 testEmbedBuilderStructure(); 413 414 const processedEmbeds = options.embeds.map((embed) => { 415 const embedObj = embed as EmbedInput; 416 logger.debug(`[Command Executor] Processing embed:`, { 417 hasData: !!(embedObj && typeof embedObj === 'object' && 'data' in embedObj), 418 embedKeys: embedObj ? Object.keys(embedObj) : [], 419 embedType: (embed as { constructor?: { name?: string } })?.constructor?.name, 420 embedPrototype: Object.getPrototypeOf(embedObj as object)?.constructor?.name, 421 }); 422 const plain = toPlainEmbedObject(embedObj); 423 return plain; 424 }); 425 426 logger.debug(`[Command Executor] Processed embeds:`, processedEmbeds); 427 428 if (processedEmbeds.length > 0) { 429 const firstEmbed = processedEmbeds[0]; 430 const firstEmbedObj = firstEmbed as Record<string, unknown>; 431 logger.debug(`[Command Executor] First processed embed details:`, { 432 title: (firstEmbedObj as { title?: unknown })?.title, 433 description: (firstEmbedObj as { description?: unknown })?.description, 434 fields: (firstEmbedObj as { fields?: unknown })?.fields, 435 fieldsLength: (firstEmbedObj as { fields?: Array<unknown> })?.fields?.length, 436 allKeys: firstEmbedObj ? Object.keys(firstEmbedObj) : [], 437 }); 438 439 logger.debug( 440 `[Command Executor] Full embed structure:`, 441 JSON.stringify(firstEmbedObj, null, 2), 442 ); 443 } 444 445 const response = { 446 success: true, 447 embeds: processedEmbeds, 448 }; 449 logger.debug(`[Command Executor] Returning embed response:`, response); 450 return JSON.stringify(response); 451 } 452 const response = { 453 success: true, 454 content: 'content' in options ? options.content : undefined, 455 }; 456 logger.debug(`[Command Executor] Returning content response:`, response); 457 return JSON.stringify(response); 458 }, 459 followUp: async (options: InteractionReplyOptions | MessagePayload) => { 460 capturedResponse = options; 461 return JSON.stringify({ 462 success: true, 463 content: 'content' in options ? options.content : undefined, 464 }); 465 }, 466 } as unknown as ChatInputCommandInteraction; 467 468 try { 469 const result = await command.execute(client, mockInteraction); 470 471 const responseToProcess = capturedResponse || result; 472 473 logger.debug(`[Command Executor] Processing response for ${name}:`, { 474 hasCapturedResponse: !!capturedResponse, 475 hasResult: result !== undefined, 476 responseType: typeof responseToProcess, 477 }); 478 479 if (!responseToProcess) { 480 return JSON.stringify({ 481 success: true, 482 message: 'Command executed successfully', 483 }); 484 } 485 486 if (typeof responseToProcess === 'string') { 487 return JSON.stringify({ 488 success: true, 489 content: responseToProcess, 490 }); 491 } 492 493 if (typeof responseToProcess === 'object') { 494 const response = responseToProcess as Record<string, unknown>; 495 496 if (Array.isArray(response.embeds) && response.embeds.length > 0) { 497 const embeds = response.embeds as Array<{ 498 title?: string; 499 description?: string; 500 fields?: Array<{ name: string; value: string; inline?: boolean }>; 501 color?: number; 502 timestamp?: string | number | Date; 503 footer?: { text: string; icon_url?: string }; 504 }>; 505 506 logger.debug(`[Command Executor] Processing embeds for ${name}:`, { 507 embedCount: embeds.length, 508 firstEmbed: embeds[0], 509 embedFields: embeds[0]?.fields, 510 embedTitle: embeds[0]?.title, 511 }); 512 513 if (name === 'weather') { 514 const embed = embeds[0]; 515 logger.debug(`[Command Executor] Weather embed details:`, { 516 hasEmbed: !!embed, 517 hasFields: !!(embed && embed.fields), 518 fieldsCount: embed?.fields?.length || 0, 519 embedData: embed, 520 }); 521 522 if (embed && embed.fields && embed.fields.length > 0) { 523 const f = embed.fields; 524 const title = embed.title || ''; 525 let locationFromTitle = ''; 526 try { 527 const m = title.match(/([A-Za-zÀ-ÖØ-öø-ÿ' .-]+,\s*[A-Z]{2})/u); 528 if (m && m[1]) { 529 locationFromTitle = m[1].trim(); 530 } 531 } catch (_err) { 532 /* ignore */ 533 } 534 let locationArg = ''; 535 if (args && typeof args === 'object') { 536 const a = args as Record<string, unknown>; 537 locationArg = String(a.location || a.query || a.search || '').trim(); 538 } 539 const weatherResponse = { 540 success: true, 541 type: 'weather', 542 location: locationFromTitle || locationArg || 'Unknown location', 543 temperature: f[0]?.value || 'N/A', 544 feels_like: f[1]?.value || 'N/A', 545 conditions: f[2]?.value || 'N/A', 546 humidity: f[3]?.value || 'N/A', 547 wind_speed: f[4]?.value || 'N/A', 548 pressure: f[5]?.value || 'N/A', 549 handled: true, 550 }; 551 logger.debug(`[Command Executor] Weather response:`, weatherResponse); 552 return JSON.stringify(weatherResponse); 553 } 554 logger.debug(`[Command Executor] Weather embed has no fields, using fallback`); 555 logger.debug(`[Command Executor] Weather embed fallback data:`, { 556 title: embed?.title, 557 description: embed?.description, 558 fields: embed?.fields, 559 allProperties: embed ? Object.keys(embed) : [], 560 }); 561 562 const fallbackResponse = { 563 success: true, 564 type: 'weather', 565 location: 566 embed?.title || 567 (args && typeof args === 'object' 568 ? String( 569 (args as Record<string, unknown>).location || 570 (args as Record<string, unknown>).query || 571 (args as Record<string, unknown>).search || 572 '', 573 ) 574 : '') || 575 'Unknown location', 576 description: embed?.description || 'Weather data unavailable', 577 rawEmbed: embed, 578 handled: true, 579 }; 580 581 return JSON.stringify(fallbackResponse); 582 } 583 584 if (name === 'joke') { 585 const embed = embeds[0]; 586 return JSON.stringify({ 587 success: true, 588 type: 'joke', 589 title: embed.title || 'Random Joke', 590 setup: embed.description || 'No joke available', 591 handled: true, 592 }); 593 } 594 595 return JSON.stringify({ 596 success: true, 597 embeds: embeds.map((embed) => ({ 598 title: embed.title, 599 description: embed.description, 600 fields: 601 embed.fields?.map((f) => ({ 602 name: f.name, 603 value: f.value, 604 inline: f.inline, 605 })) || [], 606 color: embed.color, 607 timestamp: embed.timestamp, 608 footer: embed.footer, 609 })), 610 }); 611 } 612 613 if (Array.isArray(response.components) && response.components.length > 0) { 614 if (name === '8ball') { 615 const components = response.components as Array<{ 616 components?: Array<{ 617 data?: { 618 components?: Array<{ 619 data?: { 620 content?: string; 621 }; 622 }>; 623 }; 624 }>; 625 }>; 626 627 let question = ''; 628 let answer = ''; 629 630 for (const component of components) { 631 if (component.components) { 632 for (const subComponent of component.components) { 633 if (subComponent.data?.components) { 634 for (const textComponent of subComponent.data.components) { 635 const content = textComponent.data?.content || ''; 636 if (content.includes('**Question**') || content.includes('**Pregunta**')) { 637 question = content 638 .replace(/.*?\*\*(.*?)\*\*\s*>\s*(.*?)\n\n.*/s, '$2') 639 .trim(); 640 } else if ( 641 content.includes('**Answer**') || 642 content.includes('**Respuesta**') || 643 content.includes('✨') 644 ) { 645 answer = content.replace(/.*?\s*(.*?)$/s, '$1').trim(); 646 } 647 } 648 } 649 } 650 } 651 } 652 653 return JSON.stringify({ 654 success: true, 655 type: '8ball', 656 question: question || 'Unknown question', 657 answer: answer || 'Unknown answer', 658 handled: true, 659 }); 660 } 661 } 662 663 if (typeof response.content === 'string') { 664 return JSON.stringify({ 665 success: true, 666 content: response.content, 667 }); 668 } 669 } 670 671 return JSON.stringify({ 672 success: true, 673 message: 'Command executed successfully', 674 }); 675 } catch (error) { 676 const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 677 logger.error(`Error executing command '${name}':`, errorMessage); 678 return JSON.stringify({ 679 success: false, 680 error: errorMessage, 681 status: 500, 682 }); 683 } 684 } catch (error) { 685 logger.error(`Error executing tool call '${name}':`, error); 686 return JSON.stringify({ 687 success: false, 688 error: `Error executing tool call '${name}': ${error}`, 689 status: 500, 690 }); 691 } 692}