Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
at main 395 lines 9.9 kB view raw
1import { Message } from 'discord.js'; 2import BotClient from '@/services/Client'; 3import logger from '@/utils/logger'; 4import fetch from 'node-fetch'; 5 6interface WikipediaPage { 7 pageid?: number; 8 ns?: number; 9 title: string; 10 extract?: string; 11 thumbnail?: { 12 source: string; 13 width: number; 14 height: number; 15 }; 16 pageimage?: string; 17 missing?: boolean; 18} 19 20interface _WikipediaResponse { 21 query: { 22 pages: Record<string, WikipediaPage>; 23 }; 24} 25 26interface _WeatherResponse { 27 main: { 28 temp: number; 29 feels_like: number; 30 humidity: number; 31 }; 32 weather: Array<{ 33 description: string; 34 icon: string; 35 }>; 36 wind: { 37 speed: number; 38 }; 39 name: string; 40 sys: { 41 country: string; 42 }; 43} 44 45export interface MessageToolCall { 46 name: string; 47 args: Record<string, unknown>; 48} 49 50export interface ToolResult { 51 content: Array<{ 52 type: string; 53 text?: string; 54 image_url?: { 55 url: string; 56 detail?: 'low' | 'high' | 'auto'; 57 }; 58 }>; 59 metadata: { 60 type: string; 61 url?: string; 62 title?: string; 63 subreddit?: string; 64 source?: string; 65 isSystem?: boolean; 66 [key: string]: unknown; 67 }; 68} 69 70function formatToolResponse( 71 content: string, 72 metadata: Record<string, unknown> = {}, 73 showSystemMessage = true, 74): ToolResult { 75 if (showSystemMessage) { 76 return { 77 content: [ 78 { 79 type: 'text', 80 text: `[SYSTEM] ${content}`, 81 }, 82 ], 83 metadata: { 84 ...metadata, 85 type: 'tool_response', 86 isSystem: true, 87 }, 88 }; 89 } 90 91 return { 92 content: [], 93 metadata: { 94 ...metadata, 95 type: 'tool_response', 96 isSystem: false, 97 }, 98 }; 99} 100 101async function catTool(): Promise<ToolResult> { 102 try { 103 const res = await fetch('https://api.pur.cat/random-cat'); 104 if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); 105 106 const data = (await res.json()) as { url: string; title?: string; subreddit?: string }; 107 return formatToolResponse(`Here's a cute cat for you! 🐱\n\nImage URL: ${data.url}`, { 108 type: 'cat', 109 url: data.url, 110 title: data.title, 111 subreddit: data.subreddit, 112 source: 'pur.cat', 113 }); 114 } catch (error) { 115 const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 116 logger.error('Error in cat tool:', error); 117 throw new Error(`Failed to fetch cat image: ${errorMessage}`); 118 } 119} 120 121async function dogTool(): Promise<ToolResult> { 122 try { 123 const headers = { 124 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 125 Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 126 }; 127 128 const res = await fetch('https://api.erm.dog/random-dog', { headers }); 129 if (!res.ok) throw new Error(`API request failed with status ${res.status}`); 130 131 const data = (await res.json()) as { url: string; title?: string; subreddit?: string }; 132 133 if (!data || !data.url) { 134 throw new Error('Invalid response format from dog API'); 135 } 136 137 return formatToolResponse(`Here's a cute dog for you! 🐶\n\nImage URL: ${data.url}`, { 138 type: 'dog', 139 url: data.url, 140 title: data.title || 'Random Dog', 141 subreddit: data.subreddit || 'dogpictures', 142 source: 'erm.dog', 143 }); 144 } catch (error) { 145 const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 146 logger.error('Error in dog tool:', error); 147 throw new Error(`Failed to fetch dog image: ${errorMessage}`); 148 } 149} 150 151interface WikipediaSummary { 152 title: string; 153 extract: string; 154 content_urls?: { 155 desktop?: { 156 page: string; 157 }; 158 }; 159} 160 161async function wikiTool(args: Record<string, unknown>): Promise<ToolResult> { 162 const query = args.query as string; 163 if (!query) { 164 throw new Error('Query is required for wiki tool'); 165 } 166 167 try { 168 const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`; 169 const res = await fetch(url); 170 171 if (!res.ok) { 172 throw new Error(`Wikipedia API error: ${res.status} ${res.statusText}`); 173 } 174 175 const data = (await res.json()) as WikipediaSummary; 176 const title = data.title || query; 177 const extract = data.extract || 'No summary available.'; 178 const pageUrl = 179 data.content_urls?.desktop?.page || 180 `https://en.wikipedia.org/wiki/${encodeURIComponent(query)}`; 181 182 return formatToolResponse(`${title}\n\n${extract}\n\nSource: ${pageUrl}`, { 183 type: 'wiki', 184 title, 185 extract, 186 url: pageUrl, 187 }); 188 } catch (error) { 189 const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 190 logger.error('Error in wiki tool:', error); 191 throw new Error(`Failed to search Wikipedia: ${errorMessage}`); 192 } 193} 194 195interface WeatherData { 196 main: { 197 temp: number; 198 feels_like: number; 199 humidity: number; 200 pressure: number; 201 }; 202 weather: Array<{ 203 description: string; 204 }>; 205 wind: { 206 speed: number; 207 }; 208 name: string; 209} 210 211async function weatherTool(args: Record<string, unknown>): Promise<ToolResult> { 212 const location = args.location as string; 213 if (!location) { 214 throw new Error('Location is required for weather tool'); 215 } 216 217 const apiKey = process.env.OPENWEATHER_API_KEY; 218 if (!apiKey) { 219 throw new Error('OpenWeather API key not configured'); 220 } 221 222 try { 223 const res = await fetch( 224 `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&appid=${apiKey}&units=imperial`, 225 ); 226 227 if (!res.ok) { 228 throw new Error(`Weather API error: ${res.status} ${res.statusText}`); 229 } 230 231 const data = (await res.json()) as WeatherData; 232 const temp = Math.round(data.main.temp); 233 const feels = Math.round(data.main.feels_like); 234 const conditions = data.weather[0]?.description || 'Unknown'; 235 const humidity = data.main.humidity; 236 const wind = Math.round(data.wind.speed); 237 const pressure = data.main.pressure; 238 const city = data.name || location; 239 240 return formatToolResponse( 241 `Weather for ${city}: ${temp}°F (feels ${feels}°F), ${conditions}. ` + 242 `Humidity ${humidity}%, Wind ${wind} mph, Pressure ${pressure} hPa.`, 243 { 244 type: 'weather', 245 location: city, 246 temperature: temp, 247 feels_like: feels, 248 conditions, 249 humidity, 250 wind_speed: wind, 251 pressure, 252 }, 253 ); 254 } catch (error) { 255 const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 256 logger.error('Error in weather tool:', error); 257 throw new Error(`Failed to get weather: ${errorMessage}`); 258 } 259} 260 261function resolveEmoji(input: string): string { 262 const shortcode = input.match(/^:([a-z0-9_+-]+):$/i)?.[1]; 263 if (shortcode) { 264 const map: Record<string, string> = { 265 thumbsup: '👍', 266 thumbsdown: '👎', 267 '+1': '👍', 268 '-1': '👎', 269 thumbs_up: '👍', 270 thumbs_down: '👎', 271 heart: '❤️', 272 smile: '😄', 273 grin: '😁', 274 joy: '😂', 275 cry: '😢', 276 sob: '😭', 277 clap: '👏', 278 fire: '🔥', 279 star: '⭐', 280 eyes: '👀', 281 tada: '🎉', 282 }; 283 const key = shortcode.toLowerCase(); 284 return map[key] || input; 285 } 286 return input; 287} 288 289async function reactionTool( 290 args: Record<string, unknown>, 291 message: Message, 292 client: BotClient, 293 opts?: { originalMessage?: Message; botMessage?: Message }, 294): Promise<ToolResult> { 295 const emoji = (args.emoji || args.query || '') as string; 296 const target = ((args.target as string) || 'user').toLowerCase(); 297 const targetMessage = target === 'bot' && opts?.botMessage ? opts.botMessage : message; 298 299 if (!emoji) { 300 throw new Error('Emoji is required for reaction tool'); 301 } 302 303 const resolvedEmoji = resolveEmoji(emoji); 304 305 try { 306 await targetMessage.react(resolvedEmoji); 307 308 return { 309 content: [], 310 metadata: { 311 type: 'reaction', 312 emoji: resolvedEmoji, 313 target, 314 success: true, 315 handled: true, 316 isSystem: false, 317 }, 318 }; 319 } catch (error) { 320 logger.error('Failed to add reaction:', { emoji: resolvedEmoji, error }); 321 throw new Error( 322 `Failed to add reaction: ${error instanceof Error ? error.message : 'Unknown error'}`, 323 ); 324 } 325} 326 327type ToolFunction = ( 328 args: Record<string, unknown>, 329 message: Message, 330 client: BotClient, 331 opts?: { originalMessage?: Message; botMessage?: Message }, 332) => Promise<ToolResult>; 333 334const TOOLS: Record<string, ToolFunction> = { 335 cat: catTool, 336 dog: dogTool, 337 wiki: wikiTool, 338 weather: weatherTool, 339 reaction: (args, message, client, opts) => reactionTool(args, message, client, opts), 340 newmessage: () => 341 Promise.resolve({ 342 content: [], 343 metadata: { 344 type: 'newmessage', 345 success: true, 346 handled: true, 347 isSystem: false, 348 }, 349 }), 350}; 351 352export async function executeMessageToolCall( 353 toolCall: MessageToolCall, 354 message: Message, 355 _client: BotClient, 356 _opts?: { originalMessage?: Message; botMessage?: Message }, 357): Promise<{ 358 success: boolean; 359 type: string; 360 handled: boolean; 361 error?: string; 362 result?: ToolResult; 363}> { 364 const name = (toolCall.name || '').toLowerCase(); 365 366 try { 367 const tool = TOOLS[name]; 368 if (!tool) { 369 return { 370 success: false, 371 type: 'error', 372 handled: false, 373 error: `Tool '${name}' not found`, 374 }; 375 } 376 377 const result = await tool(toolCall.args || {}, message, _client, _opts); 378 379 return { 380 success: true, 381 type: result.metadata.type || name, 382 handled: true, 383 result, 384 }; 385 } catch (error) { 386 const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 387 logger.error(`Error in executeMessageToolCall for ${name}:`, error); 388 return { 389 success: false, 390 type: 'error', 391 handled: true, 392 error: errorMessage, 393 }; 394 } 395}