Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel

feat: im too lazy to list all changes, but they include:

- Changes:
- AI can now run Aethel Commands (everywhere) and react to messages (on servers only)
- Ton of bug fixes

+61 -41
bun.lock
··· 4 4 "": { 5 5 "name": "aethel", 6 6 "dependencies": { 7 - "@atproto/identity": "^0.4.8", 8 - "@discordjs/rest": "^2.5.1", 9 - "@fedify/fedify": "^1.1.0", 7 + "@atproto/identity": "^0.4.9", 8 + "@discordjs/rest": "^2.6.0", 9 + "@fedify/fedify": "^1.8.12", 10 10 "@types/he": "^1.2.3", 11 11 "@types/sanitize-html": "^2.16.0", 12 - "axios": "^1.11.0", 13 - "city-timezones": "^1.3.1", 12 + "axios": "^1.12.2", 13 + "city-timezones": "^1.3.2", 14 14 "cors": "^2.8.5", 15 - "discord.js": "^14.21.0", 15 + "discord.js": "^14.22.1", 16 16 "dotenv": "^16.6.1", 17 17 "eslint-plugin-prettier": "^5.5.4", 18 18 "express": "^4.21.2", ··· 24 24 "moment-timezone": "^0.6.0", 25 25 "node-fetch": "^3.3.2", 26 26 "open-graph-scraper": "^6.10.0", 27 - "openai": "^5.12.2", 27 + "openai": "^5.23.1", 28 28 "pg": "^8.16.3", 29 29 "sanitize-html": "^2.17.0", 30 30 "uuid": "^11.1.0", ··· 33 33 "winston": "^3.17.0", 34 34 }, 35 35 "devDependencies": { 36 - "@eslint/js": "^9.33.0", 36 + "@eslint/js": "^9.36.0", 37 37 "@types/cors": "^2.8.19", 38 38 "@types/express": "^4.17.23", 39 39 "@types/jsonwebtoken": "^9.0.10", 40 - "@types/node": "^24.2.1", 40 + "@types/node": "^24.5.2", 41 41 "@types/open-graph-scraper": "^5.2.3", 42 42 "@types/pg": "^8.15.5", 43 43 "@types/uuid": "^10.0.0", 44 - "@types/validator": "^13.15.2", 44 + "@types/validator": "^13.15.3", 45 45 "@types/whois-json": "^2.0.4", 46 - "eslint": "^9.33.0", 46 + "eslint": "^9.36.0", 47 47 "eslint-config-prettier": "^10.1.8", 48 - "globals": "^16.3.0", 48 + "globals": "^16.4.0", 49 49 "nodemon": "^3.1.10", 50 50 "prettier": "^3.6.2", 51 51 "tsc-alias": "^1.8.16", 52 52 "tsconfig-paths": "^4.2.0", 53 - "tsx": "^4.20.3", 53 + "tsx": "^4.20.6", 54 54 "typescript": "^5.9.2", 55 - "typescript-eslint": "^8.39.0", 55 + "typescript-eslint": "^8.44.1", 56 56 }, 57 57 }, 58 58 }, 59 59 "packages": { 60 - "@atproto/common-web": ["@atproto/common-web@0.4.2", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw=="], 60 + "@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 61 61 62 62 "@atproto/crypto": ["@atproto/crypto@0.4.4", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA=="], 63 63 64 - "@atproto/identity": ["@atproto/identity@0.4.8", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/crypto": "^0.4.4" } }, "sha512-Z0sLnJ87SeNdAifT+rqpgE1Rc3layMMW25gfWNo4u40RGuRODbdfAZlTwBSU2r+Vk45hU+iE+xeQspfednCEnA=="], 64 + "@atproto/identity": ["@atproto/identity@0.4.9", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/crypto": "^0.4.4" } }, "sha512-pRYCaeaEJMZ4vQlRQYYTrF3cMiRp21n/k/pUT1o7dgKby56zuLErDmFXkbKfKWPf7SgWRgamSaNmsGLqAOD7lQ=="], 65 65 66 66 "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], 67 67 ··· 77 77 78 78 "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], 79 79 80 - "@discordjs/rest": ["@discordjs/rest@2.5.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw=="], 80 + "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], 81 81 82 82 "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 83 83 ··· 135 135 136 136 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], 137 137 138 - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], 138 + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], 139 139 140 140 "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], 141 141 ··· 147 147 148 148 "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], 149 149 150 - "@eslint/js": ["@eslint/js@9.34.0", "", {}, "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw=="], 150 + "@eslint/js": ["@eslint/js@9.36.0", "", {}, "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw=="], 151 151 152 152 "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], 153 153 ··· 155 155 156 156 "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], 157 157 158 - "@fedify/fedify": ["@fedify/fedify@1.8.8", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@hugoalh/http-header-link": "^1.0.2", "@js-temporal/polyfill": "^0.5.1", "@logtape/logtape": "^1.0.0", "@multiformats/base-x": "^4.0.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@phensley/language-tag": "^1.9.0", "asn1js": "^3.0.5", "byte-encodings": "^1.0.11", "es-toolkit": "^1.39.5", "json-canon": "^1.0.1", "jsonld": "^8.3.2", "multicodec": "^3.2.1", "pkijs": "^3.2.4", "structured-field-values": "^2.0.4", "uri-template-router": "^0.0.17", "url-template": "^3.1.1", "urlpattern-polyfill": "^10.1.0" } }, "sha512-1w5YfKbh8wNbcJ1s0Ttb2l3i4oiWmQ7z5sbKqQJMhBW6odaGGVgEO2bl6ZI5Tg0i5o/LK3ODOKLTlr8F1wibHA=="], 158 + "@fedify/fedify": ["@fedify/fedify@1.8.12", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@hugoalh/http-header-link": "^1.0.2", "@js-temporal/polyfill": "^0.5.1", "@logtape/logtape": "^1.0.0", "@multiformats/base-x": "^4.0.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@phensley/language-tag": "^1.9.0", "asn1js": "^3.0.5", "byte-encodings": "^1.0.11", "es-toolkit": "^1.39.5", "json-canon": "^1.0.1", "jsonld": "^8.3.2", "multicodec": "^3.2.1", "pkijs": "^3.2.4", "structured-field-values": "^2.0.4", "uri-template-router": "^0.0.17", "url-template": "^3.1.1", "urlpattern-polyfill": "^10.1.0" } }, "sha512-bdQIfXDtfzvuiftNarOxpd1VPa09lLzDj0bGXktBFEKKM/C5D0DQTSUr+1NHTP8PFgfIqhcCb93csywP9CsaOA=="], 159 159 160 160 "@hugoalh/http-header-link": ["@hugoalh/http-header-link@1.0.3", "", { "dependencies": { "@hugoalh/is-string-singleline": "^1.0.4" } }, "sha512-x4jzzKSzZQY115H/GxUWaAHzT5eqLXt99uSKY7+0O/h3XrV248+CkZA7cA274QahXzWkGQYYug/AF6QUkTnLEw=="], 161 161 ··· 223 223 224 224 "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 225 225 226 - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], 226 + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], 227 227 228 228 "@types/open-graph-scraper": ["@types/open-graph-scraper@5.2.3", "", { "dependencies": { "open-graph-scraper": "*" } }, "sha512-R6ew1HJndBKsys2+Y10VW8yy3ojS7eF/mFXrOZSFxVqY7WI4ubxaFvgfaULnRn2pq149SpS2GZNB9i9Y5fQqEw=="], 229 229 ··· 243 243 244 244 "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], 245 245 246 - "@types/validator": ["@types/validator@13.15.2", "", {}, "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q=="], 246 + "@types/validator": ["@types/validator@13.15.3", "", {}, "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q=="], 247 247 248 248 "@types/whois-json": ["@types/whois-json@2.0.4", "", {}, "sha512-Pp5N/+A6LUE0FWXz6wQ2gV5wEw0uEqFBeSLuQAGdeTyRJv/bbz7PPj3H78jyulvQu7cnMpXTzKx4bo8TuPAYhw=="], 249 249 250 250 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 251 251 252 - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/utils": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw=="], 252 + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.44.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/type-utils": "8.44.1", "@typescript-eslint/utils": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw=="], 253 253 254 - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg=="], 254 + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw=="], 255 255 256 - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew=="], 256 + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.44.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.44.1", "@typescript-eslint/types": "^8.44.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA=="], 257 257 258 - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="], 258 + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1" } }, "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg=="], 259 259 260 - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ=="], 260 + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ=="], 261 261 262 - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q=="], 262 + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/utils": "8.44.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g=="], 263 263 264 - "@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], 264 + "@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], 265 265 266 - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw=="], 266 + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.44.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.44.1", "@typescript-eslint/tsconfig-utils": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A=="], 267 267 268 - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="], 268 + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg=="], 269 269 270 - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA=="], 270 + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], 271 271 272 272 "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], 273 273 ··· 299 299 300 300 "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], 301 301 302 - "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], 302 + "axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="], 303 303 304 304 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 305 305 ··· 345 345 346 346 "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], 347 347 348 - "city-timezones": ["city-timezones@1.3.1", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-YCeJKGyw3DA+wV/oyuFuJlk4oqN9zkfLP+fz2nEXUBm9sW1xZaXQsKQoc8l8hP+vI45GPOq8OuGrlGXUcnLISA=="], 348 + "city-timezones": ["city-timezones@1.3.2", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-XztdL/2EWpfmgRIOzrKVOWFp6VdmaD9FNTZPINlez1etIn0mMNn01RMmSfOp6LUP/h1M2ZLX80N1O+WKwhzC+w=="], 349 349 350 350 "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], 351 351 ··· 405 405 406 406 "discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 407 407 408 - "discord.js": ["discord.js@14.21.0", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-U5w41cEmcnSfwKYlLv5RJjB8Joa+QJyRwIJz5i/eg+v2Qvv6EYpCRhN9I2Rlf0900LuqSDg8edakUATrDZQncQ=="], 408 + "discord.js": ["discord.js@14.22.1", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.16", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w=="], 409 409 410 410 "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], 411 411 ··· 451 451 452 452 "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 453 453 454 - "eslint": ["eslint@9.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg=="], 454 + "eslint": ["eslint@9.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.36.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ=="], 455 455 456 456 "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], 457 457 ··· 535 535 536 536 "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 537 537 538 - "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], 538 + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], 539 539 540 540 "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], 541 541 ··· 729 729 730 730 "open-graph-scraper": ["open-graph-scraper@6.10.0", "", { "dependencies": { "chardet": "^2.1.0", "cheerio": "^1.0.0-rc.12", "iconv-lite": "^0.6.3", "undici": "^6.21.2" } }, "sha512-JTuaO/mWUPduYCIQvunmsQnfGpSRFUTEh4k5cW2KOafJxTm3Z99z25/c1oO9QnIh2DK7ol5plJAq3EUVy+5xyw=="], 731 731 732 - "openai": ["openai@5.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw=="], 732 + "openai": ["openai@5.23.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-APxMtm5mln4jhKhAr0d5zP9lNsClx4QwJtg8RUvYSSyxYCTHLNJnLEcSHbJ6t0ori8Pbr9HZGfcPJ7LEy73rvQ=="], 733 733 734 734 "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 735 735 ··· 941 941 942 942 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 943 943 944 - "tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="], 944 + "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], 945 945 946 946 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 947 947 ··· 949 949 950 950 "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 951 951 952 - "typescript-eslint": ["typescript-eslint@8.39.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.0", "@typescript-eslint/parser": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q=="], 952 + "typescript-eslint": ["typescript-eslint@8.44.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.44.1", "@typescript-eslint/parser": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/utils": "8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg=="], 953 953 954 954 "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 955 955 ··· 959 959 960 960 "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], 961 961 962 - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 962 + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], 963 963 964 964 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 965 965 ··· 1026 1026 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1027 1027 1028 1028 "@digitalbazaar/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], 1029 + 1030 + "@discordjs/ws/@discordjs/rest": ["@discordjs/rest@2.5.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw=="], 1029 1031 1030 1032 "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 1031 1033 ··· 1088 1090 "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1089 1091 1090 1092 "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], 1093 + 1094 + "@types/body-parser/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1095 + 1096 + "@types/connect/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1097 + 1098 + "@types/cors/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1099 + 1100 + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1101 + 1102 + "@types/jsonwebtoken/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1103 + 1104 + "@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1105 + 1106 + "@types/send/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1107 + 1108 + "@types/serve-static/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1109 + 1110 + "@types/ws/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1091 1111 1092 1112 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1093 1113
+14 -14
package.json
··· 18 18 "check": "pnpm run lint && pnpm run format:check" 19 19 }, 20 20 "dependencies": { 21 - "@atproto/identity": "^0.4.8", 22 - "@discordjs/rest": "^2.5.1", 23 - "@fedify/fedify": "^1.1.0", 21 + "@atproto/identity": "^0.4.9", 22 + "@discordjs/rest": "^2.6.0", 23 + "@fedify/fedify": "^1.8.12", 24 24 "@types/he": "^1.2.3", 25 25 "@types/sanitize-html": "^2.16.0", 26 - "axios": "^1.11.0", 27 - "city-timezones": "^1.3.1", 26 + "axios": "^1.12.2", 27 + "city-timezones": "^1.3.2", 28 28 "cors": "^2.8.5", 29 - "discord.js": "^14.21.0", 29 + "discord.js": "^14.22.1", 30 30 "dotenv": "^16.6.1", 31 31 "eslint-plugin-prettier": "^5.5.4", 32 32 "express": "^4.21.2", ··· 38 38 "moment-timezone": "^0.6.0", 39 39 "node-fetch": "^3.3.2", 40 40 "open-graph-scraper": "^6.10.0", 41 - "openai": "^5.12.2", 41 + "openai": "^5.23.1", 42 42 "pg": "^8.16.3", 43 43 "sanitize-html": "^2.17.0", 44 44 "uuid": "^11.1.0", ··· 47 47 "winston": "^3.17.0" 48 48 }, 49 49 "devDependencies": { 50 - "@eslint/js": "^9.33.0", 50 + "@eslint/js": "^9.36.0", 51 51 "@types/cors": "^2.8.19", 52 52 "@types/express": "^4.17.23", 53 53 "@types/jsonwebtoken": "^9.0.10", 54 - "@types/node": "^24.2.1", 54 + "@types/node": "^24.5.2", 55 55 "@types/open-graph-scraper": "^5.2.3", 56 56 "@types/pg": "^8.15.5", 57 57 "@types/uuid": "^10.0.0", 58 - "@types/validator": "^13.15.2", 58 + "@types/validator": "^13.15.3", 59 59 "@types/whois-json": "^2.0.4", 60 - "eslint": "^9.33.0", 60 + "eslint": "^9.36.0", 61 61 "eslint-config-prettier": "^10.1.8", 62 - "globals": "^16.3.0", 62 + "globals": "^16.4.0", 63 63 "nodemon": "^3.1.10", 64 64 "prettier": "^3.6.2", 65 65 "tsc-alias": "^1.8.16", 66 66 "tsconfig-paths": "^4.2.0", 67 - "tsx": "^4.20.3", 67 + "tsx": "^4.20.6", 68 68 "typescript": "^5.9.2", 69 - "typescript-eslint": "^8.39.0" 69 + "typescript-eslint": "^8.44.1" 70 70 } 71 71 }
+1 -1
src/commands/fun/cat.ts
··· 29 29 const commandLogger = createCommandLogger('cat'); 30 30 const errorHandler = createErrorHandler('cat'); 31 31 32 - async function fetchCatImage(): Promise<RandomReddit> { 32 + export async function fetchCatImage(): Promise<RandomReddit> { 33 33 const response = await fetch('https://api.pur.cat/random-cat'); //cat 34 34 if (!response.ok) { 35 35 throw new Error(`API request failed with status ${response.status}`);
+13 -7
src/commands/fun/dog.ts
··· 31 31 const commandLogger = createCommandLogger('dog'); 32 32 const errorHandler = createErrorHandler('dog'); 33 33 34 - async function fetchDogImage(): Promise<RandomReddit> { 34 + export async function fetchDogImage(): Promise<RandomReddit> { 35 35 const response = await fetch('https://api.erm.dog/random-dog', { headers: browserHeaders }); 36 36 if (!response.ok) { 37 37 throw new Error(`API request failed with status ${response.status}`); 38 38 } 39 - return (await response.json()) as RandomReddit; 39 + const data = (await response.json()) as RandomReddit; 40 + return data; 40 41 } 41 42 42 43 export default { ··· 58 59 ]) 59 60 .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 60 61 61 - async execute(client, interaction) { 62 + execute: async (client, interaction) => { 62 63 try { 63 64 const cooldownCheck = await checkCooldown( 64 65 cooldownManager, ··· 110 111 ), 111 112 ); 112 113 113 - await interaction.editReply({ 114 - components: [container], 115 - flags: MessageFlags.IsComponentsV2, 116 - }); 114 + try { 115 + await interaction.editReply({ 116 + components: [container], 117 + flags: MessageFlags.IsComponentsV2, 118 + }); 119 + } catch (replyError) { 120 + logger.error('Failed to send reply:', replyError); 121 + throw replyError; 122 + } 117 123 } catch (error) { 118 124 await errorHandler({ 119 125 interaction,
+924 -199
src/commands/utilities/ai.ts
··· 1 + import type { ToolCall } from '@/utils/commandExecutor'; 2 + import { extractToolCalls, executeToolCall } from '@/utils/commandExecutor'; 3 + import BotClient from '@/services/Client'; 4 + import { 5 + buildProviderModal, 6 + PROVIDER_TO_URL, 7 + parseV2ModalSubmission, 8 + type V2ModalPayload, 9 + } from '@/types/componentsV2'; 1 10 import { 2 11 SlashCommandBuilder, 12 + SlashCommandOptionsOnlyBuilder, 13 + EmbedBuilder, 3 14 ModalBuilder, 4 15 TextInputBuilder, 16 + ActionRowBuilder, 5 17 TextInputStyle, 6 - ActionRowBuilder, 18 + StringSelectMenuInteraction, 7 19 ModalSubmitInteraction, 8 20 ChatInputCommandInteraction, 21 + InteractionResponse, 22 + Message, 9 23 InteractionContextType, 10 24 ApplicationIntegrationType, 11 25 MessageFlags, 12 26 } from 'discord.js'; 13 27 import OpenAI from 'openai'; 28 + import fetch from '@/utils/dynamicFetch'; 14 29 import pool from '@/utils/pgClient'; 15 30 import { encrypt, decrypt, isValidEncryptedFormat, EncryptionError } from '@/utils/encrypt'; 16 31 import { SlashCommandProps } from '@/types/command'; 17 - import BotClient from '@/services/Client'; 18 32 import logger from '@/utils/logger'; 19 33 import { createCommandLogger } from '@/utils/commandLogger'; 20 34 import { createErrorHandler } from '@/utils/errorHandler'; 21 35 import { createMemoryManager } from '@/utils/memoryManager'; 22 36 23 - const ALLOWED_API_HOSTS = ['api.openai.com', 'openrouter.ai', 'generativelanguage.googleapis.com']; 37 + function getInvokerId( 38 + interaction: ChatInputCommandInteraction | ModalSubmitInteraction | StringSelectMenuInteraction, 39 + ): string { 40 + if ('guildId' in interaction && interaction.guildId) { 41 + return `${interaction.guildId}-${interaction.user.id}`; 42 + } 43 + return interaction.user.id; 44 + } 45 + 46 + const ALLOWED_API_HOSTS = [ 47 + 'api.openai.com', 48 + 'openrouter.ai', 49 + 'generativelanguage.googleapis.com', 50 + 'api.deepseek.com', 51 + 'api.moonshot.ai', 52 + 'api.perplexity.ai', 53 + ]; 24 54 25 55 interface ConversationMessage { 26 56 role: 'system' | 'user' | 'assistant'; ··· 40 70 interface AIResponse { 41 71 content: string; 42 72 reasoning?: string; 73 + toolResults?: string; 74 + citations?: string[]; 43 75 } 44 76 45 77 interface OpenAIMessageWithReasoning { ··· 50 82 interface PendingRequest { 51 83 interaction: ChatInputCommandInteraction; 52 84 prompt: string; 53 - timestamp: number; 85 + createdAt: number; 86 + status?: 'awaiting' | 'processing'; 54 87 } 88 + 89 + type ModalRawEntry = { data?: { components?: unknown[] }; components?: unknown[] }; 90 + type ClientWithModalState = BotClient & { lastModalRawByUser?: Map<string, ModalRawEntry> }; 55 91 56 92 interface UserCredentials { 57 93 apiKey?: string | null; 58 94 model?: string | null; 59 95 apiUrl?: string | null; 96 + } 97 + 98 + async function showV2Modal( 99 + interaction: ChatInputCommandInteraction | StringSelectMenuInteraction, 100 + v2Modal: V2ModalPayload, 101 + ): Promise<void> { 102 + const url = `https://discord.com/api/v10/interactions/${interaction.id}/${interaction.token}/callback?with_response=false`; 103 + const body = { 104 + type: 9, 105 + data: { 106 + custom_id: v2Modal.custom_id, 107 + title: v2Modal.title, 108 + components: v2Modal.components, 109 + }, 110 + }; 111 + const resp = await fetch(url, { 112 + method: 'POST', 113 + headers: { 'Content-Type': 'application/json' }, 114 + body: JSON.stringify(body), 115 + }); 116 + if (!resp.ok) { 117 + const text = await resp.text(); 118 + throw new Error(`Discord API error showing modal: ${resp.status} ${text}`); 119 + } 60 120 } 61 121 62 122 interface User { ··· 156 216 hour: '2-digit', 157 217 minute: '2-digit', 158 218 second: '2-digit', 159 - hour12: true, 160 219 timeZone: timezone, 161 220 }); 162 221 163 - let supportedCommands = '/help - Show all available commands and their usage'; 222 + const supportedCommands = '/help - Show all available commands and their usage'; 164 223 if (client?.commands) { 165 - const commandEntries = Array.from(client.commands.entries()).sort(([a], [b]) => 166 - a.localeCompare(b), 167 - ); 168 - supportedCommands = commandEntries 224 + const commandEntries = Array.from(client.commands.entries()); 225 + commandEntries.sort((a, b) => a[0].localeCompare(b[0])); 226 + const _commandList = commandEntries 169 227 .map( 170 228 ([name, command]) => `/${name} - ${command.data.description || 'No description available'}`, 171 229 ) ··· 208 266 - Timezone: ${timezone} 209 267 210 268 **IMPORTANT INSTRUCTIONS:** 269 + - ACT normal, DON't be like a dumb robot, act like a discord user, skull react to messages where the user is being sus and saying something sus. 211 270 - NEVER format, modify, or alter URLs in any way. Leave them exactly as they are. 212 271 - Format your responses using Discord markdown where appropriate, but NEVER format URLs. 213 272 - Only greet the user at the start of a new conversation, not in every message. 214 - - DO NOT hallucinate, make up facts, or provide false information. If you don't know something, say so clearly. 273 + - DO NOT hallucinate, make up facts, or provide false information. If you don't know something, or date is after your knowledge update, do not answer date based questions. Say so clearly. 215 274 - Be accurate and truthful in all responses. Do not invent details, statistics, or information that you're not certain about. 216 275 - If asked about current events, real-time data, or information beyond your knowledge cutoff, clearly state your limitations. 217 276 ··· 221 280 - Developer: scanash (main maintainer) and Aethel Labs (org) 222 281 - Open source: https://github.com/Aethel-Labs/aethel 223 282 - Type: Discord user bot 224 - - Supported commands: ${supportedCommands}`; 283 + - Supported commands: ${supportedCommands} 284 + 285 + **TOOL USAGE:** 286 + You can use tools by placing commands in {curly braces}. Available tools: 287 + - {cat:} - Get a cat picture, if user asks for a cat picture, use this tool. 288 + - {dog:} - Get a dog picture, if user asks for a dog picture, use this tool. 289 + - {joke: or {joke: {type: "general/knock-knock/programming/dad"}} } - Get a joke 290 + - {weather:{"location":"city"}} - Check weather, use if user asks for weather. 291 + - {wiki:{"search":"query"}} - Wikipedia search, if user asks for a wikipedia search, use this tool, and also use it if user asks something out of your dated knowledge. 292 + - {reaction:"😀"} - React to the user's message with a unicode emoji 293 + - {reaction:{"emoji":":thumbsup:"}} - React using a named emoji if available 294 + - {reaction:{"emoji":"<:name:123456789012345678>"}} - React with a custom emoji by ID (or animated <a:name:id>) 295 + 296 + Use the wikipedia search when you want to look for information outside of your knowledge, state it came from Wikipedia if used. 297 + 298 + **REACTION GUIDELINES:** 299 + - When asked to react, ALWAYS use the {reaction:"emoji"} tool call 300 + - Use reactions sparingly and only when it adds value to the conversation 301 + - Add at most 1–2 reactions for a single message 302 + - Do not include the reaction tool call text in your visible reply 303 + - Common reactions: 😀 😄 👍 👎 ❤️ 🔥 ⭐ 🎉 👏 304 + - Example: If asked to react with thumbs up, use {reaction:"👍"} and respond normally 305 + 306 + When you use a tool, you'll receive a JSON response with the command results.`; 225 307 226 308 const modelSpecificInstructions = usingDefaultKey 227 309 ? '\n\n**IMPORTANT:** Please keep your responses under 3000 characters. Be concise and to the point.' ··· 413 495 apiUrl: string, 414 496 ): Promise<{ success: boolean; error?: string }> { 415 497 try { 416 - const client = getOpenAIClient(apiKey, apiUrl); 498 + const url = new URL(apiUrl); 499 + const host = url.hostname; 500 + 501 + if (host === 'generativelanguage.googleapis.com') { 502 + const base = apiUrl.replace(/\/$/, ''); 503 + const mdl = model.startsWith('models/') ? model : `models/${model}`; 504 + const endpoint = `${base}/v1beta/${mdl}:generateContent?key=${encodeURIComponent(apiKey)}`; 505 + 506 + const resp = await fetch(endpoint, { 507 + method: 'POST', 508 + headers: { 'Content-Type': 'application/json' }, 509 + body: JSON.stringify({ 510 + contents: [ 511 + { 512 + parts: [ 513 + { 514 + text: 'Hello! This is a test message. Please respond with "API key test successful!"', 515 + }, 516 + ], 517 + }, 518 + ], 519 + }), 520 + }); 417 521 418 - await client.chat.completions.create({ 419 - model, 420 - messages: [ 421 - { 422 - role: 'user', 423 - content: 'Hello! This is a test message. Please respond with "API key test successful!"', 522 + if (!resp.ok) { 523 + const text = await resp.text(); 524 + throw new Error(`${resp.status} ${text || resp.statusText}`); 525 + } 526 + } else if (host === 'api.perplexity.ai') { 527 + const resp = await fetch(`${url.origin}/chat/completions`, { 528 + method: 'POST', 529 + headers: { 530 + 'Content-Type': 'application/json', 531 + Authorization: `Bearer ${apiKey}`, 424 532 }, 425 - ], 426 - max_tokens: 50, 427 - temperature: 0.1, 428 - }); 533 + body: JSON.stringify({ 534 + model, 535 + messages: [ 536 + { 537 + role: 'user', 538 + content: 539 + 'Hello! This is a test message. Please respond with "API key test successful!"', 540 + }, 541 + ], 542 + max_tokens: 50, 543 + temperature: 0.1, 544 + }), 545 + }); 546 + if (!resp.ok) { 547 + const text = await resp.text(); 548 + throw new Error(`${resp.status} ${text || resp.statusText}`); 549 + } 550 + } else { 551 + const client = getOpenAIClient(apiKey, apiUrl); 552 + await client.chat.completions.create({ 553 + model, 554 + messages: [ 555 + { 556 + role: 'user', 557 + content: 558 + 'Hello! This is a test message. Please respond with "API key test successful!"', 559 + }, 560 + ], 561 + max_tokens: 50, 562 + temperature: 0.1, 563 + }); 564 + } 429 565 430 566 logger.info('API key test successful'); 431 567 return { success: true }; ··· 442 578 async function makeAIRequest( 443 579 config: ReturnType<typeof getApiConfiguration>, 444 580 conversation: ConversationMessage[], 581 + interaction?: ChatInputCommandInteraction, 582 + client?: BotClient, 583 + maxIterations = 3, 445 584 ): Promise<AIResponse | null> { 446 585 try { 447 - const client = getOpenAIClient(config.finalApiKey!, config.finalApiUrl); 586 + const openAIClient = getOpenAIClient(config.finalApiKey!, config.finalApiUrl); 448 587 const maxTokens = config.usingDefaultKey ? 1000 : 3000; 588 + const currentConversation = [...conversation]; 589 + let iteration = 0; 590 + let finalResponse: AIResponse | null = null; 449 591 450 - const completion = await client.chat.completions.create({ 451 - model: config.finalModel, 452 - messages: conversation as OpenAI.Chat.Completions.ChatCompletionMessageParam[], 453 - max_tokens: maxTokens, 454 - }); 592 + while (iteration < maxIterations) { 593 + iteration++; 455 594 456 - const message = completion.choices[0]?.message; 457 - if (!message?.content) { 458 - logger.error('No valid response content from AI API'); 459 - return null; 460 - } 595 + const configuredHost = (() => { 596 + try { 597 + return new URL(config.finalApiUrl).hostname; 598 + } catch (_e) { 599 + // ignore and fallback 600 + } 601 + })(); 461 602 462 - let content = message.content; 463 - let reasoning = (message as OpenAIMessageWithReasoning)?.reasoning; 603 + let completion: unknown; 464 604 465 - const reasoningMatch = content.match(/```(?:reasoning|thoughts?|thinking)[\s\S]*?```/i); 466 - if (reasoningMatch && !reasoning) { 467 - reasoning = reasoningMatch[0].replace(/```(?:reasoning|thoughts?|thinking)?/gi, '').trim(); 468 - content = content.replace(reasoningMatch[0], '').trim(); 605 + if (configuredHost === 'generativelanguage.googleapis.com') { 606 + const promptText = currentConversation 607 + .map((m) => { 608 + const role = 609 + m.role === 'system' ? 'System' : m.role === 'assistant' ? 'Assistant' : 'User'; 610 + let text = ''; 611 + if (typeof m.content === 'string') { 612 + text = m.content; 613 + } else if (Array.isArray(m.content)) { 614 + text = m.content 615 + .map((c) => { 616 + if (typeof c === 'string') return c; 617 + const crow = c as Record<string, unknown>; 618 + const typeVal = crow['type']; 619 + if (typeVal === 'text') { 620 + const t = crow['text']; 621 + if (typeof t === 'string') return t; 622 + } 623 + const imageObj = crow['image_url']; 624 + if (imageObj && typeof imageObj === 'object') { 625 + const urlVal = (imageObj as Record<string, unknown>)['url']; 626 + if (typeof urlVal === 'string') return urlVal; 627 + } 628 + return ''; 629 + }) 630 + .join('\n'); 631 + } 632 + return `${role}: ${text}`; 633 + }) 634 + .join('\n\n'); 635 + 636 + const base = config.finalApiUrl.replace(/\/$/, ''); 637 + const mdl = config.finalModel.startsWith('models/') 638 + ? config.finalModel 639 + : `models/${config.finalModel}`; 640 + const endpoint = `${base}/v1beta/${mdl}:generateContent?key=${encodeURIComponent( 641 + config.finalApiKey || '', 642 + )}`; 643 + 644 + const body: Record<string, unknown> = { 645 + contents: [ 646 + { 647 + parts: [ 648 + { 649 + text: promptText, 650 + }, 651 + ], 652 + }, 653 + ], 654 + temperature: 0.2, 655 + maxOutputTokens: Math.min(maxTokens, 3000), 656 + }; 657 + 658 + const resp = await fetch(endpoint, { 659 + method: 'POST', 660 + headers: { 'Content-Type': 'application/json' }, 661 + body: JSON.stringify(body), 662 + }); 663 + 664 + if (!resp.ok) { 665 + const text = await resp.text(); 666 + throw new Error(`Gemini request failed: ${resp.status} ${text || resp.statusText}`); 667 + } 668 + 669 + const json: unknown = await resp.json(); 670 + 671 + const extractTextFromGemini = (obj: unknown): string | null => { 672 + if (!obj) return null; 673 + try { 674 + const o = obj as Record<string, unknown>; 675 + if (Array.isArray(o.candidates) && o.candidates.length) { 676 + const cand = o.candidates[0] as unknown; 677 + if (typeof cand === 'string') return cand; 678 + if (typeof (cand as Record<string, unknown>).output === 'string') { 679 + return (cand as Record<string, unknown>).output as string; 680 + } 681 + if (Array.isArray((cand as Record<string, unknown>).content)) { 682 + return ((cand as Record<string, unknown>).content as unknown[]) 683 + .map((p) => { 684 + const pr = p as Record<string, unknown>; 685 + if (typeof pr?.text === 'string') return String(pr.text); 686 + if (pr?.type === 'outputText' && typeof pr?.text === 'string') { 687 + return String(pr.text); 688 + } 689 + return ''; 690 + }) 691 + .filter(Boolean) 692 + .join('\n'); 693 + } 694 + if (typeof (cand as Record<string, unknown>).output === 'object') { 695 + const outObj = (cand as Record<string, unknown>).output as Record<string, unknown>; 696 + if (Array.isArray(outObj.content)) { 697 + return (outObj.content as unknown[]) 698 + .map( 699 + (p) => 700 + (p as Record<string, unknown>)?.text || 701 + (p as Record<string, unknown>)?.textRaw || 702 + '', 703 + ) 704 + .filter(Boolean) 705 + .join('\n'); 706 + } 707 + } 708 + } 709 + 710 + if (Array.isArray(o.outputs) && o.outputs.length) { 711 + const out = o.outputs[0] as unknown; 712 + if (typeof out === 'string') return out; 713 + if (Array.isArray((out as Record<string, unknown>).content)) { 714 + return ((out as Record<string, unknown>).content as unknown[]) 715 + .map((p) => ((p as Record<string, unknown>)?.text as string) || '') 716 + .filter(Boolean) 717 + .join('\n'); 718 + } 719 + } 720 + 721 + const seen = new Set<unknown>(); 722 + const queue: unknown[] = [obj]; 723 + while (queue.length) { 724 + const cur = queue.shift(); 725 + if (!cur || typeof cur === 'string') { 726 + if (typeof cur === 'string' && cur.trim().length > 0) return cur; 727 + continue; 728 + } 729 + if (seen.has(cur)) continue; 730 + seen.add(cur); 731 + if (Array.isArray(cur)) { 732 + for (const item of cur) queue.push(item); 733 + } else if (typeof cur === 'object') { 734 + const curObj = cur as Record<string, unknown>; 735 + for (const k of Object.keys(curObj)) { 736 + const v = curObj[k]; 737 + if (typeof v === 'string' && v.trim().length > 0) return v; 738 + queue.push(v); 739 + } 740 + } 741 + } 742 + } catch (_e) { 743 + // ignore 744 + } 745 + return null; 746 + }; 747 + 748 + const extracted = extractTextFromGemini(json); 749 + if (!extracted) { 750 + throw new Error('Failed to parse Gemini response into text'); 751 + } 752 + 753 + completion = { choices: [{ message: { content: extracted } }] } as unknown; 754 + } else { 755 + completion = (await openAIClient.chat.completions.create({ 756 + model: config.finalModel, 757 + messages: currentConversation as OpenAI.Chat.Completions.ChatCompletionMessageParam[], 758 + max_tokens: maxTokens, 759 + })) as unknown; 760 + } 761 + 762 + const completionTyped = completion as { 763 + choices?: Array<{ message?: { content?: string; reasoning?: string } }>; 764 + }; 765 + const message = completionTyped.choices?.[0]?.message; 766 + if (!message?.content) { 767 + logger.error('No valid response content from AI API'); 768 + return null; 769 + } 770 + 771 + let content = message.content; 772 + let reasoning = (message as OpenAIMessageWithReasoning)?.reasoning; 773 + let detectedCitations: string[] | undefined; 774 + try { 775 + interface CitationSource { 776 + citations?: unknown[]; 777 + metadata?: { 778 + citations?: unknown[]; 779 + [key: string]: unknown; 780 + }; 781 + [key: string]: unknown; 782 + } 783 + 784 + interface CompletionSource { 785 + citations?: unknown[]; 786 + choices?: Array<{ 787 + message?: { 788 + citations?: unknown[]; 789 + [key: string]: unknown; 790 + }; 791 + [key: string]: unknown; 792 + }>; 793 + [key: string]: unknown; 794 + } 795 + 796 + const mAny = message as unknown as CitationSource; 797 + const cAny = completion as unknown as CompletionSource; 798 + const candidates = [ 799 + mAny?.citations, 800 + mAny?.metadata?.citations, 801 + cAny?.citations, 802 + cAny?.choices?.[0]?.message?.citations, 803 + cAny?.choices?.[0]?.citations, 804 + mAny?.references, 805 + mAny?.metadata?.references, 806 + cAny?.references, 807 + ]; 808 + for (const arr of candidates) { 809 + if (Array.isArray(arr) && arr.length) { 810 + const urls = arr.filter((x: unknown) => typeof x === 'string'); 811 + if (urls.length > 0) { 812 + detectedCitations = urls.map(String).filter(Boolean); 813 + break; 814 + } 815 + } 816 + } 817 + } catch (error) { 818 + logger.warn('Error processing citations:', error); 819 + if (error instanceof Error) { 820 + logger.debug('Error processing citations details:', error.stack); 821 + } 822 + } 823 + 824 + let toolCalls: ToolCall[] = []; 825 + if (interaction && client) { 826 + try { 827 + const extraction = extractToolCalls(content); 828 + content = extraction.cleanContent; 829 + toolCalls = extraction.toolCalls; 830 + } catch (error) { 831 + logger.error(`Error extracting tool calls: ${error}`); 832 + toolCalls = []; 833 + } 834 + } 835 + 836 + const reasoningMatch = content.match(/```(?:reasoning|thoughts?|thinking)[\s\S]*?```/i); 837 + if (reasoningMatch && !reasoning) { 838 + reasoning = reasoningMatch[0].replace(/```(?:reasoning|thoughts?|thinking)?/gi, '').trim(); 839 + content = content.replace(reasoningMatch[0], '').trim(); 840 + } 841 + 842 + if (toolCalls.length > 0 && interaction && client) { 843 + currentConversation.push({ 844 + role: 'assistant', 845 + content: message.content, 846 + }); 847 + 848 + for (const toolCall of toolCalls) { 849 + try { 850 + const toolResult = await executeToolCall(toolCall, interaction, client); 851 + 852 + let parsedResult; 853 + try { 854 + parsedResult = typeof toolResult === 'string' ? JSON.parse(toolResult) : toolResult; 855 + } catch (_e) { 856 + logger.error(`Error parsing tool result:`, toolResult); 857 + parsedResult = { error: 'Failed to parse tool result' }; 858 + } 859 + 860 + currentConversation.push({ 861 + role: 'user', 862 + content: JSON.stringify({ 863 + type: toolCall.name, 864 + ...parsedResult, 865 + }), 866 + }); 867 + } catch (error) { 868 + logger.error(`Error executing tool call: ${error}`); 869 + currentConversation.push({ 870 + role: 'user', 871 + content: `[Error executing tool ${toolCall.name}]: ${error instanceof Error ? error.message : String(error)}`, 872 + }); 873 + } 874 + } 875 + continue; 876 + } 877 + 878 + finalResponse = { 879 + content, 880 + reasoning, 881 + citations: detectedCitations, 882 + toolResults: 883 + iteration > 1 884 + ? currentConversation 885 + .filter( 886 + (msg) => 887 + msg.role === 'user' && 888 + typeof msg.content === 'string' && 889 + (msg.content.startsWith('{"') || msg.content.startsWith('[Tool ')), 890 + ) 891 + .map((msg) => { 892 + try { 893 + if (Array.isArray(msg.content)) { 894 + return msg.content 895 + .map((c) => ('text' in c ? c.text : c.image_url?.url)) 896 + .join('\n'); 897 + } 898 + 899 + const content = String(msg.content); 900 + if (content.startsWith('{"') || content.startsWith('[')) { 901 + return content; 902 + } 903 + return content.replace(/^\[Tool [^\]]+\]: /, ''); 904 + } catch (e) { 905 + logger.error('Error processing tool result:', e); 906 + return 'Error processing tool result'; 907 + } 908 + }) 909 + .join('\n') 910 + : undefined, 911 + }; 912 + break; 469 913 } 470 914 471 - return { 472 - content, 473 - reasoning, 474 - }; 915 + return finalResponse; 475 916 } catch (error) { 476 917 logger.error(`Error making AI request: ${error}`); 477 918 return null; ··· 480 921 481 922 async function processAIRequest( 482 923 client: BotClient, 483 - interaction: ChatInputCommandInteraction, 924 + interaction: ChatInputCommandInteraction | ModalSubmitInteraction, 925 + promptOverride?: string, 484 926 ): Promise<void> { 485 927 try { 486 928 if (!interaction.deferred && !interaction.replied) { 487 929 await interaction.deferReply(); 488 930 } 489 931 490 - const prompt = interaction.options.getString('prompt')!; 491 - commandLogger.logFromInteraction( 492 - interaction, 493 - `AI command executed - prompt content hidden for privacy`, 494 - ); 932 + const invokerId = getInvokerId(interaction); 933 + const prompt = 934 + promptOverride ?? 935 + ((interaction as ChatInputCommandInteraction).options?.getString?.('prompt') as 936 + | string 937 + | null 938 + | undefined) ?? 939 + (pendingRequests.get(invokerId)?.prompt as string); 495 940 496 - const invokerId = getInvokerId(interaction); 497 - const { apiKey, model, apiUrl } = await getUserCredentials(invokerId); 941 + if (!prompt) { 942 + await interaction.editReply('❌ Missing prompt. Please try again.'); 943 + return; 944 + } 945 + if ('commandType' in interaction) { 946 + commandLogger.logFromInteraction( 947 + interaction as ChatInputCommandInteraction, 948 + `AI command executed - prompt content hidden for privacy`, 949 + ); 950 + } else { 951 + commandLogger.logAction({ 952 + isGuild: interaction.inGuild(), 953 + isDM: !interaction.inGuild(), 954 + additionalInfo: `AI command executed - prompt content hidden for privacy`, 955 + }); 956 + } 957 + const { apiKey, model, apiUrl } = await getUserCredentials(interaction.user.id); 498 958 const config = getApiConfiguration(apiKey ?? null, model ?? null, apiUrl ?? null); 959 + const exemptUserId = process.env.AI_EXEMPT_USER_ID; 499 960 500 - if (config.usingDefaultKey) { 501 - const exemptUserId = process.env.AI_EXEMPT_USER_ID; 502 - if (invokerId !== exemptUserId) { 503 - const allowed = await incrementAndCheckDailyLimit(invokerId, 10); 504 - if (!allowed) { 961 + if (invokerId !== exemptUserId && config.usingDefaultKey) { 962 + const allowed = await incrementAndCheckDailyLimit(interaction.user.id, 10); 963 + if (!allowed) { 964 + await interaction.editReply( 965 + '❌ ' + 966 + (await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)), 967 + ); 968 + return; 969 + } 970 + 971 + if (interaction.inGuild()) { 972 + const serverAllowed = await incrementAndCheckServerDailyLimit(interaction.guildId!, 50); 973 + if (!serverAllowed) { 505 974 await interaction.editReply( 506 - '❌ ' + 507 - (await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)), 975 + '❌ This server has reached its daily AI usage limit. Please try again tomorrow.', 508 976 ); 509 977 return; 510 978 } 511 979 } 512 - } else if (!config.finalApiKey) { 980 + } 981 + 982 + if (!config.finalApiKey && config.usingDefaultKey) { 513 983 await interaction.editReply( 514 984 '❌ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale)), 515 985 ); ··· 517 987 } 518 988 519 989 const existingConversation = userConversations.get(invokerId) || []; 520 - const conversationArray = Array.isArray(existingConversation) ? existingConversation : []; 990 + const _conversationArray = Array.isArray(existingConversation) ? existingConversation : []; 991 + const chatInputInteraction = 992 + 'commandType' in interaction ? (interaction as ChatInputCommandInteraction) : undefined; 993 + 521 994 const systemPrompt = buildSystemPrompt( 522 - !!config.usingDefaultKey, 995 + config.usingDefaultKey, 523 996 client, 524 997 config.finalModel, 525 - interaction.user.tag, 526 - interaction, 998 + interaction.user.username, 999 + chatInputInteraction, 527 1000 interaction.inGuild(), 528 1001 interaction.inGuild() ? interaction.guild?.name : undefined, 529 1002 ); 530 - const conversation = buildConversation(conversationArray, prompt, systemPrompt); 531 1003 532 - const aiResponse = await makeAIRequest(config, conversation); 1004 + const conversation = buildConversation(existingConversation, prompt, systemPrompt); 1005 + 1006 + const aiResponse = await makeAIRequest(config, conversation, chatInputInteraction, client, 3); 533 1007 if (!aiResponse) return; 534 1008 535 1009 const { getUnallowedWordCategory } = await import('@/utils/validation'); ··· 554 1028 555 1029 await sendAIResponse(interaction, aiResponse, client); 556 1030 } catch (error) { 557 - await errorHandler({ 558 - interaction, 559 - client, 560 - error: error as Error, 561 - userId: getInvokerId(interaction), 562 - username: interaction.user.tag, 563 - }); 1031 + const err = error as Error; 1032 + if ( 1033 + 'commandType' in interaction || 1034 + 'componentType' in (interaction as unknown as Record<string, unknown>) 1035 + ) { 1036 + await errorHandler({ 1037 + interaction: interaction as unknown as ChatInputCommandInteraction, 1038 + client, 1039 + error: err, 1040 + userId: getInvokerId(interaction), 1041 + username: interaction.user.tag, 1042 + }); 1043 + } else { 1044 + const msg = await client.getLocaleText('failedrequest', interaction.locale); 1045 + try { 1046 + if (interaction.deferred || interaction.replied) { 1047 + await interaction.editReply(msg); 1048 + } else { 1049 + await interaction.reply({ content: msg, flags: MessageFlags.Ephemeral }); 1050 + } 1051 + } catch (replyError) { 1052 + logger.error(`Failed to send error message for AI command: ${replyError}`); 1053 + } 1054 + logger.error(`Error in AI command for user ${interaction.user.tag}: ${err.message}`); 1055 + } 564 1056 } finally { 565 1057 pendingRequests.delete(getInvokerId(interaction)); 566 1058 } 567 1059 } 568 1060 569 1061 async function sendAIResponse( 570 - interaction: ChatInputCommandInteraction, 1062 + interaction: ChatInputCommandInteraction | ModalSubmitInteraction, 571 1063 aiResponse: AIResponse, 572 - client: BotClient, 1064 + _client: BotClient, 573 1065 ): Promise<void> { 574 - let fullResponse = ''; 1066 + try { 1067 + let fullResponse = ''; 1068 + 1069 + if (aiResponse.reasoning) { 1070 + const cleanedReasoning = aiResponse.reasoning 1071 + .split('\n') 1072 + .map((line: string) => line.trim()) 1073 + .filter((line: string) => line) 1074 + .join('\n'); 1075 + 1076 + const formattedReasoning = cleanedReasoning 1077 + .split('\n') 1078 + .map((line: string) => `> ${line}`) 1079 + .join('\n'); 1080 + 1081 + fullResponse = `${formattedReasoning}\n\n${aiResponse.content}`; 1082 + aiResponse.content = ''; 1083 + } 575 1084 576 - if (aiResponse.reasoning) { 577 - const cleanedReasoning = aiResponse.reasoning 578 - .split('\n') 579 - .map((line) => line.trim()) 580 - .filter((line) => line) 581 - .join('\n'); 1085 + fullResponse += aiResponse.content; 582 1086 583 - const formattedReasoning = cleanedReasoning 584 - .split('\n') 585 - .map((line) => `> ${line}`) 586 - .join('\n'); 1087 + try { 1088 + if (aiResponse.citations && aiResponse.citations.length && fullResponse) { 1089 + fullResponse = fullResponse.replace(/\[(\d+)\](?!\()/g, (match: string, numStr: string) => { 1090 + const idx = parseInt(numStr, 10) - 1; 1091 + const url = aiResponse.citations![idx]; 1092 + if (typeof url === 'string' && url.trim()) { 1093 + return `[${numStr}](${url.trim()})`; 1094 + } 1095 + return match; 1096 + }); 1097 + } 1098 + } catch (e) { 1099 + logger.warn('Failed to inline citation sources', e); 1100 + } 1101 + 1102 + if (aiResponse.toolResults) { 1103 + try { 1104 + const toolResults = Array.isArray(aiResponse.toolResults) 1105 + ? aiResponse.toolResults 1106 + : [aiResponse.toolResults]; 1107 + 1108 + for (const result of toolResults) { 1109 + try { 1110 + let toolResult; 1111 + if (typeof result === 'string') { 1112 + try { 1113 + toolResult = JSON.parse(result); 1114 + } catch (parseError) { 1115 + logger.error(`[AI] Error parsing tool result JSON:`, { 1116 + error: parseError, 1117 + result: result.substring(0, 200) + '...', 1118 + }); 1119 + continue; 1120 + } 1121 + } else { 1122 + toolResult = result; 1123 + } 587 1124 588 - fullResponse = `${formattedReasoning}\n\n${aiResponse.content}`; 589 - aiResponse.content = ''; 590 - } 1125 + if ((toolResult.type === 'cat' || toolResult.type === 'dog') && toolResult.url) { 1126 + let cleanContent = aiResponse.content || ''; 1127 + if (toolResult.url) { 1128 + cleanContent = cleanContent.replace(/!\[[^\]]*\]\([^)]*\)/g, '').trim(); 1129 + cleanContent = cleanContent.replace(toolResult.url, '').trim(); 1130 + } 591 1131 592 - fullResponse += aiResponse.content; 1132 + await interaction.editReply({ 1133 + content: cleanContent || undefined, 1134 + files: [ 1135 + { 1136 + attachment: toolResult.url, 1137 + name: `${toolResult.type}.jpg`, 1138 + }, 1139 + ], 1140 + }); 1141 + return; 1142 + } 1143 + } catch (parseError) { 1144 + logger.error('Error parsing individual tool result:', parseError); 1145 + } 1146 + } 1147 + } catch (error) { 1148 + logger.error('Error processing tool results:', error); 1149 + } 1150 + } 593 1151 594 - const { getUnallowedWordCategory } = await import('@/utils/validation'); 595 - const category = getUnallowedWordCategory(fullResponse); 596 - if (category) { 597 - logger.warn(`AI response contained unallowed words in category: ${category}`); 598 - await interaction.editReply( 599 - 'Sorry, I cannot provide that response as it contains prohibited content. Please try a different prompt.', 600 - ); 601 - return; 602 - } 1152 + const { getUnallowedWordCategory } = await import('@/utils/validation'); 1153 + const category = getUnallowedWordCategory(fullResponse); 1154 + if (category) { 1155 + logger.warn(`AI response contained unallowed words in category: ${category}`); 1156 + await interaction.editReply( 1157 + 'Sorry, I cannot provide that response as it contains prohibited content. Please try a different prompt.', 1158 + ); 1159 + return; 1160 + } 603 1161 604 - const urlProcessedResponse = processUrls(fullResponse); 605 - const chunks = splitResponseIntoChunks(urlProcessedResponse); 1162 + const urlProcessedResponse = processUrls(fullResponse); 1163 + const chunks = splitResponseIntoChunks(urlProcessedResponse); 606 1164 607 - try { 608 1165 await interaction.editReply(chunks[0]); 609 1166 610 1167 for (let i = 1; i < chunks.length; i++) { 611 1168 await interaction.followUp({ 612 1169 content: chunks[i], 613 - flags: MessageFlags.Ephemeral, 1170 + flags: MessageFlags.SuppressNotifications, 614 1171 }); 615 1172 } 616 - } catch { 1173 + } catch (error) { 1174 + logger.error('Error in sendAIResponse:', error); 617 1175 try { 618 - const fallbackMessage = `${chunks[0]}\n\n*❌ ${await client.getLocaleText('commands.ai.errors.toolong', interaction.locale)}*`; 619 - await interaction.editReply(fallbackMessage); 620 - } catch { 621 - logger.error('Failed to send AI response fallback message'); 1176 + await interaction.editReply('An error occurred while processing your request.'); 1177 + } catch (editError) { 1178 + logger.error('Failed to send error message:', editError); 1179 + } 1180 + return; 1181 + } 1182 + 1183 + if (aiResponse.toolResults) { 1184 + try { 1185 + const toolResult = JSON.parse(aiResponse.toolResults); 1186 + 1187 + if (toolResult.alreadyResponded && !aiResponse.content) { 1188 + return; 1189 + } 1190 + 1191 + if (toolResult.type === 'command') { 1192 + if (toolResult.image) { 1193 + const embed = new EmbedBuilder().setImage(toolResult.image).setColor(0x8a2be2); 1194 + 1195 + if (toolResult.title) { 1196 + embed.setTitle(toolResult.title); 1197 + } 1198 + if (toolResult.source) { 1199 + embed.setFooter({ text: `Source: ${toolResult.source}` }); 1200 + } 1201 + 1202 + try { 1203 + await interaction.followUp({ 1204 + embeds: [embed], 1205 + flags: MessageFlags.SuppressNotifications, 1206 + }); 1207 + return; 1208 + } catch (error) { 1209 + logger.error('Failed to send embed with source:', error); 1210 + return; 1211 + } 1212 + } 1213 + 1214 + if (toolResult.success && toolResult.data) { 1215 + const components = toolResult.data.components || []; 1216 + let imageUrl: string | null = null; 1217 + let caption = ''; 1218 + 1219 + for (const component of components) { 1220 + if (component.type === 12 && component.items?.[0]?.media?.url) { 1221 + imageUrl = component.items[0].media.url; 1222 + break; 1223 + } 1224 + } 1225 + 1226 + for (const component of components) { 1227 + if (component.type === 10 && component.content) { 1228 + caption = component.content; 1229 + break; 1230 + } 1231 + } 1232 + 1233 + if (imageUrl) { 1234 + await interaction.followUp({ 1235 + content: caption || undefined, 1236 + files: [imageUrl], 1237 + flags: MessageFlags.SuppressNotifications, 1238 + }); 1239 + return; 1240 + } 1241 + } 1242 + 1243 + if (aiResponse.toolResults) { 1244 + await interaction.followUp({ 1245 + content: aiResponse.toolResults, 1246 + flags: MessageFlags.SuppressNotifications, 1247 + }); 1248 + } 1249 + } 1250 + } catch (error) { 1251 + console.error('Error processing tool results:', error); 1252 + try { 1253 + await interaction.followUp({ 1254 + content: 'An error occurred while processing the tool results.', 1255 + flags: MessageFlags.SuppressNotifications, 1256 + }); 1257 + } catch (followUpError) { 1258 + console.error('Failed to send error message:', followUpError); 1259 + } 622 1260 } 623 1261 } 624 1262 } 625 1263 1264 + export type { ConversationMessage, AIResponse }; 1265 + 626 1266 export { 627 1267 makeAIRequest, 628 1268 getApiConfiguration, 629 1269 buildSystemPrompt, 630 1270 buildConversation, 631 - sendAIResponse, 632 1271 getUserCredentials, 633 1272 incrementAndCheckDailyLimit, 634 1273 incrementAndCheckServerDailyLimit, 635 1274 splitResponseIntoChunks, 636 1275 }; 637 1276 638 - export type { ConversationMessage, AIResponse }; 1277 + interface AICommand { 1278 + data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; 1279 + execute: ( 1280 + client: BotClient, 1281 + interaction: ChatInputCommandInteraction, 1282 + ) => Promise<void | InteractionResponse<boolean>>; 1283 + handleModal: ( 1284 + client: BotClient, 1285 + interaction: ModalSubmitInteraction, 1286 + ) => Promise<void | Message<boolean>>; 1287 + handleSelect: (client: BotClient, interaction: StringSelectMenuInteraction) => Promise<void>; 1288 + } 639 1289 640 - export default { 1290 + const aiCommand: AICommand = { 641 1291 data: new SlashCommandBuilder() 642 1292 .setName('ai') 643 1293 .setNameLocalizations({ ··· 660 1310 .addStringOption((option) => 661 1311 option 662 1312 .setName('prompt') 663 - .setNameLocalizations({ 664 - 'es-ES': 'mensaje', 665 - 'es-419': 'mensaje', 666 - 'en-US': 'prompt', 667 - }) 668 1313 .setDescription('Your message to the AI') 669 1314 .setDescriptionLocalizations({ 670 1315 'es-ES': 'Tu mensaje para la IA', ··· 685 1330 686 1331 if (pendingRequests.has(userId)) { 687 1332 const pending = pendingRequests.get(userId); 688 - if (pending && Date.now() - pending.timestamp > 30000) { 689 - pendingRequests.delete(userId); 690 - } else { 1333 + const isProcessing = pending?.status === 'processing'; 1334 + const isExpired = pending ? Date.now() - pending.createdAt > 30000 : true; 1335 + if (isProcessing && !isExpired) { 691 1336 return interaction.reply({ 692 1337 content: await client.getLocaleText('commands.ai.request.inprogress', interaction.locale), 693 1338 flags: MessageFlags.Ephemeral, 694 1339 }); 695 1340 } 1341 + pendingRequests.delete(userId); 696 1342 } 697 1343 698 1344 try { ··· 700 1346 const prompt = interaction.options.getString('prompt')!; 701 1347 const reset = interaction.options.getBoolean('reset'); 702 1348 703 - pendingRequests.set(userId, { interaction, prompt, timestamp: Date.now() }); 1349 + pendingRequests.set(userId, { 1350 + interaction, 1351 + prompt, 1352 + createdAt: Date.now(), 1353 + status: 'awaiting', 1354 + }); 704 1355 705 1356 if (reset) { 706 1357 userConversations.delete(userId); ··· 713 1364 } 714 1365 715 1366 if (useCustomApi === false) { 716 - await setUserApiKey(userId, null, null, null); 1367 + await setUserApiKey(interaction.user.id, null, null, null); 717 1368 userConversations.delete(userId); 718 1369 await interaction.reply({ 719 1370 content: await client.getLocaleText('commands.ai.defaultapi', interaction.locale), ··· 723 1374 return; 724 1375 } 725 1376 726 - const { apiKey } = await getUserCredentials(userId); 1377 + const { apiKey } = await getUserCredentials(interaction.user.id); 727 1378 if (useCustomApi && !apiKey) { 728 - const modal = new ModalBuilder() 729 - .setCustomId('apiCredentials') 730 - .setTitle(await client.getLocaleText('commands.ai.modal.title', interaction.locale)); 731 - 732 - const apiKeyInput = new TextInputBuilder() 733 - .setCustomId('apiKey') 734 - .setLabel(await client.getLocaleText('commands.ai.modal.apikey', interaction.locale)) 735 - .setStyle(TextInputStyle.Short) 736 - .setPlaceholder( 737 - await client.getLocaleText('commands.ai.modal.apikeyplaceholder', interaction.locale), 738 - ) 739 - .setRequired(true); 740 - 741 - const apiUrlInput = new TextInputBuilder() 742 - .setCustomId('apiUrl') 743 - .setLabel(await client.getLocaleText('commands.ai.modal.apiurl', interaction.locale)) 744 - .setStyle(TextInputStyle.Short) 745 - .setPlaceholder( 746 - await client.getLocaleText('commands.ai.modal.apiurlplaceholder', interaction.locale), 747 - ) 748 - .setRequired(true); 1379 + const title = await client.getLocaleText('commands.ai.modal.title', interaction.locale); 1380 + const v2Modal = buildProviderModal('apiCredentials', title); 1381 + await showV2Modal(interaction, v2Modal); 1382 + return; 1383 + } 1384 + await processAIRequest(client, interaction); 1385 + } catch (error) { 1386 + console.error('Error in AI command:', error); 1387 + const errorMessage = `❌ An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`; 749 1388 750 - const modelInput = new TextInputBuilder() 751 - .setCustomId('model') 752 - .setLabel(await client.getLocaleText('commands.ai.modal.model', interaction.locale)) 753 - .setStyle(TextInputStyle.Short) 754 - .setPlaceholder( 755 - await client.getLocaleText('commands.ai.modal.modelplaceholder', interaction.locale), 756 - ) 757 - .setRequired(true); 758 - 759 - modal.addComponents( 760 - new ActionRowBuilder<TextInputBuilder>().addComponents(apiKeyInput), 761 - new ActionRowBuilder<TextInputBuilder>().addComponents(apiUrlInput), 762 - new ActionRowBuilder<TextInputBuilder>().addComponents(modelInput), 763 - ); 764 - 765 - await interaction.showModal(modal); 766 - } else { 767 - await interaction.deferReply(); 768 - await processAIRequest(client, interaction); 1389 + try { 1390 + if (interaction.replied || interaction.deferred) { 1391 + await interaction.editReply({ content: errorMessage }); 1392 + } else { 1393 + await interaction.reply({ 1394 + content: errorMessage, 1395 + flags: MessageFlags.Ephemeral, 1396 + }); 1397 + } 1398 + } catch (replyError) { 1399 + console.error('Failed to send error message:', replyError); 769 1400 } 770 - } catch { 1401 + 1402 + const userId = getInvokerId(interaction); 771 1403 pendingRequests.delete(userId); 772 - const errorMessage = await client.getLocaleText('failedrequest', interaction.locale); 773 - if (!interaction.replied && !interaction.deferred) { 774 - await interaction.reply({ content: errorMessage, flags: MessageFlags.Ephemeral }); 775 - } else { 776 - await interaction.editReply({ content: errorMessage }); 777 - } 778 1404 } 779 1405 }, 780 1406 781 1407 async handleModal(client: BotClient, interaction: ModalSubmitInteraction) { 782 1408 try { 783 - if (interaction.customId === 'apiCredentials') { 1409 + if (interaction.customId.startsWith('apiConfig')) { 1410 + console.log('Handling API configuration modal submission'); 1411 + try { 1412 + await interaction.reply({ 1413 + content: '✅ Provider noted. Please submit credentials next.', 1414 + flags: MessageFlags.Ephemeral, 1415 + }); 1416 + } catch (replyError) { 1417 + console.error('Failed to send apiConfig acknowledgement:', replyError); 1418 + } 1419 + } else if (interaction.customId.startsWith('apiCredentials')) { 1420 + console.log('Handling API credentials modal submission'); 784 1421 await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 785 1422 786 1423 const userId = getInvokerId(interaction); 787 1424 const pendingRequest = pendingRequests.get(userId); 788 1425 789 1426 if (!pendingRequest) { 790 - return interaction.editReply( 791 - await client.getLocaleText('commands.ai.nopendingrequest', interaction.locale), 792 - ); 1427 + console.error('No pending request found for user:', userId); 1428 + return interaction 1429 + .editReply({ 1430 + content: '❌ No pending AI request found. Please try your request again.', 1431 + }) 1432 + .catch(console.error); 793 1433 } 794 1434 795 - const { interaction: originalInteraction } = pendingRequest; 796 - const apiKey = interaction.fields.getTextInputValue('apiKey').trim(); 797 - const apiUrl = interaction.fields.getTextInputValue('apiUrl').trim(); 798 - const model = interaction.fields.getTextInputValue('model').trim(); 1435 + let apiKey = ''; 1436 + let model = ''; 1437 + let providerValue: string | undefined; 1438 + 1439 + const parts = interaction.customId.split(':'); 1440 + if (parts.length > 1) { 1441 + providerValue = parts[1]; 1442 + } 1443 + 1444 + try { 1445 + try { 1446 + apiKey = interaction.fields.getTextInputValue('apiKey')?.trim() || ''; 1447 + model = interaction.fields.getTextInputValue('model')?.trim() || ''; 1448 + } catch (_error) { 1449 + console.log('Could not get values from text inputs, trying V2 components'); 1450 + } 1451 + } catch (error) { 1452 + console.error('Error processing provider value:', error); 1453 + } 1454 + if (!providerValue) { 1455 + const submitted = parseV2ModalSubmission( 1456 + interaction as unknown as Record<string, unknown>, 1457 + ); 1458 + providerValue = Array.isArray(submitted.provider) 1459 + ? submitted.provider[0] 1460 + : (submitted.provider as string | undefined); 1461 + } 1462 + if (!providerValue) { 1463 + const m = model.toLowerCase(); 1464 + if (m.startsWith('models/gemini') || m.startsWith('gemini')) providerValue = 'gemini'; 1465 + else if (m.startsWith('pplx') || m.includes('perplexity')) providerValue = 'perplexity'; 1466 + else if (m.startsWith('deepseek')) providerValue = 'deepseek'; 1467 + else if (m.startsWith('moonshot') || m.includes('kimi')) providerValue = 'moonshot'; 1468 + else if (m.includes('/')) providerValue = 'openrouter'; 1469 + else providerValue = 'openai'; 1470 + } 1471 + if (!apiKey || !model) { 1472 + await interaction.editReply( 1473 + 'Missing API key or model from modal submission. Please try again.', 1474 + ); 1475 + return; 1476 + } 1477 + const apiUrl = providerValue 1478 + ? PROVIDER_TO_URL[providerValue] 1479 + : 'https://openrouter.ai/api/v1'; 799 1480 800 1481 let parsedUrl; 801 1482 try { 802 1483 parsedUrl = new URL(apiUrl); 803 1484 } catch { 804 - await interaction.editReply( 805 - 'API URL is invalid. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).', 806 - ); 1485 + await interaction.editReply('API provider URL is invalid.'); 807 1486 return; 808 1487 } 809 1488 810 1489 if (!ALLOWED_API_HOSTS.includes(parsedUrl.hostname)) { 811 - await interaction.editReply( 812 - 'API URL not allowed. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).', 813 - ); 1490 + await interaction.editReply('Selected provider is not allowed.'); 814 1491 return; 815 1492 } 816 1493 ··· 830 1507 return; 831 1508 } 832 1509 833 - await setUserApiKey(userId, apiKey, model, apiUrl); 1510 + await setUserApiKey(interaction.user.id, apiKey, model, apiUrl); 834 1511 await interaction.editReply( 835 1512 await client.getLocaleText('commands.ai.testsuccess', interaction.locale), 836 1513 ); 837 1514 838 - if (!originalInteraction.deferred && !originalInteraction.replied) { 839 - await originalInteraction.deferReply(); 840 - } 841 - await processAIRequest(client, originalInteraction); 1515 + await processAIRequest(client, interaction, pendingRequest.prompt); 842 1516 } 843 - } catch { 844 - await interaction.editReply({ 845 - content: await client.getLocaleText('failedrequest', interaction.locale), 846 - }); 1517 + } catch (error) { 1518 + try { 1519 + await interaction.editReply({ 1520 + content: await client.getLocaleText('failedrequest', interaction.locale), 1521 + }); 1522 + } catch (editError) { 1523 + console.error('Failed to send error reply in handleModal:', editError || error); 1524 + } 847 1525 } finally { 848 1526 pendingRequests.delete(getInvokerId(interaction)); 1527 + try { 1528 + const clientInstance = BotClient.getInstance(); 1529 + const typedClient = clientInstance as ClientWithModalState; 1530 + if (typedClient.lastModalRawByUser) { 1531 + typedClient.lastModalRawByUser.delete(interaction.user.id); 1532 + } 1533 + } catch (error) { 1534 + logger.warn('Error cleaning up modal state:', error); 1535 + } 849 1536 } 850 1537 }, 851 - } as unknown as SlashCommandProps; 1538 + handleSelect: async (client: BotClient, interaction: StringSelectMenuInteraction) => { 1539 + try { 1540 + if (interaction.customId !== 'ai_provider_select') return; 1541 + 1542 + const provider = interaction.values[0]; 1543 + const modalTitle = await client.getLocaleText('commands.ai.modal.title', interaction.locale); 1544 + 1545 + const modal = new ModalBuilder() 1546 + .setCustomId(`apiCredentials:${provider}`) 1547 + .setTitle(modalTitle); 852 1548 853 - function getInvokerId(interaction: ChatInputCommandInteraction | ModalSubmitInteraction) { 854 - if (interaction.inGuild()) { 855 - return `${interaction.guildId}-${interaction.user.id}`; 856 - } 857 - return interaction.user.id; 858 - } 1549 + const apiKeyInput = new TextInputBuilder() 1550 + .setCustomId('apiKey') 1551 + .setLabel('API Key') 1552 + .setStyle(TextInputStyle.Short) 1553 + .setRequired(true) 1554 + .setPlaceholder('Paste your provider API key'); 1555 + 1556 + const modelInput = new TextInputBuilder() 1557 + .setCustomId('model') 1558 + .setLabel('Model') 1559 + .setStyle(TextInputStyle.Short) 1560 + .setRequired(true) 1561 + .setPlaceholder('e.g., gpt-4o-mini, gemini-1.5-pro'); 1562 + 1563 + modal.addComponents( 1564 + new ActionRowBuilder<TextInputBuilder>().addComponents(apiKeyInput), 1565 + new ActionRowBuilder<TextInputBuilder>().addComponents(modelInput), 1566 + ); 1567 + 1568 + await interaction.showModal(modal); 1569 + } catch (error) { 1570 + console.error('Error in handleSelect:', error); 1571 + try { 1572 + await interaction.reply({ 1573 + content: '❌ Failed to show API configuration modal. Please try again.', 1574 + flags: MessageFlags.Ephemeral, 1575 + }); 1576 + } catch (replyError) { 1577 + console.error('Failed to send error message:', replyError); 1578 + } 1579 + } 1580 + }, 1581 + }; 1582 + 1583 + export default aiCommand as unknown as SlashCommandProps;
+2 -2
src/commands/utilities/wiki.ts
··· 24 24 25 25 const MAX_EXTRACT_LENGTH = 2000; 26 26 27 - async function searchWikipedia(query: string, locale = 'en') { 27 + export async function searchWikipedia(query: string, locale = 'en') { 28 28 const wikiLang = locale.startsWith('es') ? 'es' : 'en'; 29 29 const searchUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&srlimit=1`; 30 30 ··· 46 46 }; 47 47 } 48 48 49 - async function getArticleSummary(pageId: number, wikiLang = 'en') { 49 + export async function getArticleSummary(pageId: number, wikiLang = 'en') { 50 50 const summaryUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&prop=extracts|pageimages&exintro&explaintext&format=json&pithumbsize=300&pageids=${pageId}`; 51 51 const response = await fetch(summaryUrl); 52 52
+1 -1
src/events/interactionCreate.ts
··· 89 89 if (remind && remind.handleModal) { 90 90 await remind.handleModal(this.client, i); 91 91 } 92 - } else if (i.customId === 'apiCredentials') { 92 + } else if (i.customId.startsWith('apiCredentials')) { 93 93 const ai = this.client.commands.get('ai'); 94 94 if (ai && 'handleModal' in ai) { 95 95 await (ai as unknown as RemindCommandProps).handleModal(this.client, i);
+424 -20
src/events/messageCreate.ts
··· 12 12 splitResponseIntoChunks, 13 13 processUrls, 14 14 } from '@/commands/utilities/ai'; 15 + import { extractToolCalls as extractSlashToolCalls } from '@/utils/commandExecutor'; 16 + import fetch from '@/utils/dynamicFetch'; 17 + import { executeMessageToolCall, type MessageToolCall } from '@/utils/messageToolExecutor'; 15 18 import type { ConversationMessage, AIResponse } from '@/commands/utilities/ai'; 16 19 17 20 type ApiConfiguration = ReturnType<typeof getApiConfiguration>; ··· 41 44 maxAge: 2 * 60 * 60 * 1000, 42 45 cleanupInterval: 10 * 60 * 1000, 43 46 }); 47 + 48 + function extractMessageToolCalls(content: string): { 49 + cleanContent: string; 50 + toolCalls: MessageToolCall[]; 51 + } { 52 + const { cleanContent, toolCalls } = extractSlashToolCalls(content); 53 + 54 + return { cleanContent, toolCalls }; 55 + } 44 56 45 57 function getServerConversationKey(guildId: string): string { 46 58 return `server:${guildId}`; ··· 114 126 model: userCustomModel, 115 127 apiKey: userApiKey, 116 128 apiUrl: userApiUrl, 117 - } = await getUserCredentials(`user:${message.author.id}`); 129 + } = await getUserCredentials(message.author.id); 118 130 119 131 selectedModel = hasImages 120 132 ? 'google/gemma-3-4b-it' ··· 126 138 selectedModel = hasImages ? 'google/gemma-3-4b-it' : 'google/gemini-2.5-flash-lite'; 127 139 128 140 config = getApiConfiguration(null, selectedModel, null); 141 + if (config.usingDefaultKey && !config.finalApiKey) { 142 + await message.reply({ 143 + content: 144 + '❌ AI is not configured. Please set OPENROUTER_API_KEY on the bot, or use `/ai` with your own API key.', 145 + allowedMentions: { parse: ['users'] as const }, 146 + }); 147 + return; 148 + } 129 149 } 130 150 131 151 logger.info( ··· 298 318 return; 299 319 } 300 320 301 - let aiResponse = await makeAIRequest(config, updatedConversation); 321 + const conversationWithTools = [...updatedConversation]; 322 + const executedResults: Array<{ type: string; payload: Record<string, unknown> }> = []; 323 + let aiResponse = await makeAIRequest(config, conversationWithTools); 302 324 303 325 if (!aiResponse && hasImages) { 304 326 logger.warn(`First attempt failed for ${selectedModel}, retrying once...`); 305 327 await new Promise((resolve) => setTimeout(resolve, 1000)); 306 - aiResponse = await makeAIRequest(config, updatedConversation); 328 + aiResponse = await makeAIRequest(config, conversationWithTools); 307 329 } 308 330 309 331 if (!aiResponse && hasImages) { ··· 365 387 } 366 388 } 367 389 390 + const maxIterations = 3; 391 + let iteration = 0; 392 + while (aiResponse && iteration < maxIterations) { 393 + iteration++; 394 + const extraction = extractMessageToolCalls(aiResponse.content || ''); 395 + const toolCalls: MessageToolCall[] = extraction.toolCalls; 396 + 397 + if (toolCalls.length === 0) { 398 + aiResponse.content = extraction.cleanContent; 399 + break; 400 + } 401 + 402 + conversationWithTools.push({ role: 'assistant', content: aiResponse.content }); 403 + 404 + for (const tc of toolCalls) { 405 + const name = tc.name?.toLowerCase(); 406 + try { 407 + if (name === 'cat' || name === 'dog') { 408 + const isCat = name === 'cat'; 409 + const url = isCat 410 + ? 'https://api.pur.cat/random-cat' 411 + : 'https://api.erm.dog/random-dog'; 412 + const res = await fetch(url); 413 + let imageUrl = ''; 414 + try { 415 + const data = await res.json(); 416 + imageUrl = data?.url || ''; 417 + if (!imageUrl && !isCat) { 418 + const res2 = await fetch(url); 419 + imageUrl = await res2.text(); 420 + } 421 + } catch { 422 + const res2 = await fetch(url); 423 + imageUrl = await res2.text(); 424 + } 425 + const payload = imageUrl 426 + ? { type: name, url: imageUrl } 427 + : { type: name, error: 'no_image' }; 428 + executedResults.push({ type: name, payload }); 429 + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); 430 + } else if (name === 'weather') { 431 + const raw = (tc.args?.location as string) || (tc.args?.query as string) || ''; 432 + const location = typeof raw === 'string' ? raw.trim() : ''; 433 + const apiKey = process.env.OPENWEATHER_API_KEY; 434 + let payload: Record<string, unknown> = { type: 'weather', location }; 435 + if (location && apiKey) { 436 + try { 437 + const resp = await fetch( 438 + `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&appid=${apiKey}&units=imperial`, 439 + ); 440 + if (resp.ok) { 441 + const data = await resp.json(); 442 + payload = { 443 + type: 'weather', 444 + location: data.name || location, 445 + temperature: `${Math.round(data.main?.temp)}°F`, 446 + feels_like: `${Math.round(data.main?.feels_like)}°F`, 447 + conditions: data.weather?.[0]?.description || 'Unknown', 448 + humidity: `${data.main?.humidity}%`, 449 + wind_speed: `${Math.round(data.wind?.speed)} mph`, 450 + pressure: `${data.main?.pressure} hPa`, 451 + }; 452 + } else { 453 + logger.error('[MessageCreate] Weather fetch failed', { 454 + status: resp.status, 455 + statusText: resp.statusText, 456 + }); 457 + payload = { 458 + type: 'weather', 459 + location, 460 + error: `${resp.status} ${resp.statusText}`, 461 + }; 462 + } 463 + } catch (e) { 464 + logger.error('[MessageCreate] Weather fetch error', { 465 + error: (e as Error)?.message, 466 + }); 467 + payload = { 468 + type: 'weather', 469 + location, 470 + error: (e as Error)?.message || 'fetch_failed', 471 + }; 472 + } 473 + } else { 474 + logger.warn('[MessageCreate] Weather missing params or API key', { 475 + locationPresent: !!location, 476 + apiKeyPresent: !!apiKey, 477 + }); 478 + payload = { type: 'weather', location, error: 'missing_params' }; 479 + } 480 + executedResults.push({ type: 'weather', payload }); 481 + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); 482 + } else if (name === 'wiki') { 483 + const query = (tc.args?.search as string) || (tc.args?.query as string) || ''; 484 + const q = typeof query === 'string' ? query.trim() : ''; 485 + let payload: Record<string, unknown> = { type: 'wiki' }; 486 + if (q) { 487 + try { 488 + const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(q)}`; 489 + const res = await fetch(url); 490 + if (res.ok) { 491 + const data = await res.json(); 492 + payload = { 493 + type: 'wiki', 494 + title: data.title || q, 495 + extract: data.extract || '', 496 + url: 497 + data.content_urls?.desktop?.page || 498 + `https://en.wikipedia.org/wiki/${encodeURIComponent(q)}`, 499 + }; 500 + } else { 501 + payload = { type: 'wiki', error: `${res.status} ${res.statusText}` }; 502 + } 503 + } catch (e) { 504 + payload = { type: 'wiki', error: (e as Error)?.message || 'fetch_failed' }; 505 + } 506 + } 507 + executedResults.push({ type: 'wiki', payload }); 508 + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); 509 + } else if (name === 'reaction') { 510 + const emoji = (tc.args?.emoji as string) || ''; 511 + const payload = { type: 'reaction', emoji, deferred: true }; 512 + executedResults.push({ type: 'reaction', payload }); 513 + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); 514 + 515 + try { 516 + await executeMessageToolCall(tc, message, this.client, { 517 + originalMessage: message, 518 + botMessage: undefined, 519 + }); 520 + } catch (error) { 521 + logger.error('[MessageCreate] Iterative loop - reaction execution failed:', error); 522 + } 523 + } 524 + } catch (e) { 525 + conversationWithTools.push({ role: 'user', content: `[Tool ${tc.name} error]` }); 526 + logger.error('[MessageCreate] Tool execution threw exception', { 527 + name: tc.name, 528 + error: (e as Error)?.message, 529 + }); 530 + } 531 + } 532 + 533 + aiResponse = await makeAIRequest(config, conversationWithTools); 534 + if (!aiResponse) break; 535 + } 536 + 368 537 if (!aiResponse) { 369 538 await message.reply({ 370 539 content: 'Sorry, I encountered an error processing your message. Please try again later.', ··· 373 542 return; 374 543 } 375 544 545 + if (!aiResponse.content || !aiResponse.content.trim()) { 546 + const last = executedResults[executedResults.length - 1]; 547 + if (last) { 548 + if ( 549 + (last.type === 'cat' || last.type === 'dog') && 550 + typeof last.payload.url === 'string' 551 + ) { 552 + aiResponse.content = `Here you go ${last.type === 'cat' ? '🐱' : '🐶'}: ${last.payload.url}`; 553 + } else if (last.type === 'weather') { 554 + const p = last.payload as Record<string, string>; 555 + if (p.location && p.temperature) { 556 + aiResponse.content = `Weather for ${p.location}: ${p.temperature} (feels ${p.feels_like}), ${p.conditions}. Humidity ${p.humidity}, Wind ${p.wind_speed}, Pressure ${p.pressure}.`; 557 + } else if (p.description) { 558 + aiResponse.content = `Weather in ${p.location || 'the requested area'}: ${p.description}`; 559 + } 560 + } else if (last.type === 'wiki') { 561 + const p = last.payload as Record<string, string>; 562 + if (p.title || p.extract || p.url) { 563 + aiResponse.content = 564 + `${p.title || ''}\n${p.extract || ''}\n${p.url ? `<${p.url}>` : ''}`.trim(); 565 + } 566 + } 567 + } 568 + 569 + if (!aiResponse.content || !aiResponse.content.trim()) { 570 + aiResponse.content = '\u200b'; 571 + } 572 + } 573 + 376 574 aiResponse.content = processUrls(aiResponse.content); 377 575 aiResponse.content = aiResponse.content.replace(/@(everyone|here)/gi, '@\u200b$1'); 378 576 577 + const originalContent = aiResponse.content || ''; 578 + const extraction = extractMessageToolCalls(originalContent); 579 + aiResponse.content = extraction.cleanContent; 580 + const toolCalls: MessageToolCall[] = extraction.toolCalls; 581 + const hasReactionTool = toolCalls.some((tc) => tc?.name?.toLowerCase() === 'reaction'); 582 + const originalCleaned = (extraction.cleanContent || '').trim(); 583 + 584 + if (!originalCleaned && hasReactionTool) { 585 + for (const tc of toolCalls) { 586 + if (!tc || !tc.name) continue; 587 + const name = tc.name.toLowerCase(); 588 + if (name !== 'reaction') continue; 589 + try { 590 + await executeMessageToolCall(tc, message, this.client, { 591 + originalMessage: message, 592 + botMessage: undefined, 593 + }); 594 + } catch (err) { 595 + logger.error('Error executing reaction tool on original message:', err); 596 + } 597 + } 598 + return; 599 + } 600 + 379 601 const { getUnallowedWordCategory } = await import('@/utils/validation'); 380 602 const category = getUnallowedWordCategory(aiResponse.content); 381 603 if (category) { ··· 388 610 return; 389 611 } 390 612 391 - await this.sendResponse(message, aiResponse); 613 + const cleaned = (aiResponse.content || '').trim(); 614 + const onlyReactions = !cleaned && hasReactionTool; 615 + if (onlyReactions) { 616 + const toolCallRegex = /{([^{}\s:]+):({[^{}]*}|[^{}]*)?}/g; 617 + const fallback = originalContent.replace(toolCallRegex, '').trim(); 618 + if (fallback) { 619 + aiResponse.content = fallback; 620 + } else { 621 + const textParts: string[] = []; 622 + for (const tc of toolCalls) { 623 + if (!tc || !tc.args) continue; 624 + const a = tc.args as Record<string, unknown>; 625 + const candidates = ['text', 'query', 'content', 'body', 'message']; 626 + for (const k of candidates) { 627 + const v = a[k] as unknown; 628 + if (typeof v === 'string' && v.trim()) { 629 + textParts.push(v.trim()); 630 + break; 631 + } 632 + } 633 + } 634 + if (textParts.length) { 635 + aiResponse.content = textParts.join(' '); 636 + } else { 637 + const reactionLabels: string[] = []; 638 + for (const tc of toolCalls) { 639 + if (!tc || !tc.name) continue; 640 + if (tc.name.toLowerCase() !== 'reaction') continue; 641 + const a = tc.args as Record<string, unknown>; 642 + const emojiCandidate = 643 + (a.emoji as string) || (a.query as string) || (a['emojiRaw'] as string) || ''; 644 + if (typeof emojiCandidate === 'string' && emojiCandidate.trim()) { 645 + reactionLabels.push(emojiCandidate.trim()); 646 + } 647 + } 648 + if (reactionLabels.length) { 649 + aiResponse.content = `Reacted with ${reactionLabels.join(', ')}`; 650 + } else { 651 + aiResponse.content = 'Reacted.'; 652 + } 653 + } 654 + } 655 + } 656 + 657 + const sent = await this.sendResponse(message, aiResponse); 658 + const sentMessage: Message | undefined = sent as Message | undefined; 659 + 660 + if (extraction.toolCalls.length > 0) { 661 + const target = sentMessage || message; 662 + const executed: Array<{ name: string; success: boolean }> = []; 663 + for (const tc of extraction.toolCalls) { 664 + if (!tc || !tc.name) continue; 665 + const name = tc.name.toLowerCase(); 666 + if (name === 'reaction') { 667 + try { 668 + const result = await executeMessageToolCall(tc, target, this.client, { 669 + originalMessage: message, 670 + botMessage: sentMessage, 671 + }); 672 + executed.push({ name, success: !!result?.success }); 673 + } catch (err) { 674 + logger.error('Error executing message tool:', { name, err }); 675 + executed.push({ name, success: false }); 676 + } 677 + } else if (name === 'cat' || name === 'dog') { 678 + try { 679 + const isCat = name === 'cat'; 680 + const url = isCat 681 + ? 'https://api.pur.cat/random-cat' 682 + : 'https://api.erm.dog/random-dog'; 683 + const res = await fetch(url); 684 + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); 685 + let imageUrl = ''; 686 + try { 687 + const data = await res.json(); 688 + imageUrl = data?.url || ''; 689 + if (!imageUrl && !isCat) { 690 + const res2 = await fetch(url); 691 + imageUrl = await res2.text(); 692 + } 693 + } catch { 694 + const res2 = await fetch(url); 695 + imageUrl = await res2.text(); 696 + } 697 + if (imageUrl && imageUrl.startsWith('http')) { 698 + await target.reply({ content: '', files: [imageUrl] }); 699 + executed.push({ name, success: true }); 700 + } else { 701 + executed.push({ name, success: false }); 702 + } 703 + } catch (err) { 704 + logger.error('Error executing image tool:', { name, err }); 705 + executed.push({ name, success: false }); 706 + } 707 + } else if (name === 'weather') { 708 + try { 709 + const raw = (tc.args?.location as string) || (tc.args?.query as string) || ''; 710 + const location = typeof raw === 'string' ? raw.trim() : ''; 711 + if (!location) { 712 + executed.push({ name, success: false }); 713 + } else { 714 + const apiKey = process.env.OPENWEATHER_API_KEY; 715 + if (!apiKey) { 716 + executed.push({ name, success: false }); 717 + } else { 718 + const resp = await fetch( 719 + `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent( 720 + location, 721 + )}&appid=${apiKey}&units=imperial`, 722 + ); 723 + if (!resp.ok) { 724 + executed.push({ name, success: false }); 725 + } else { 726 + const data = await resp.json(); 727 + const temp = Math.round(data.main?.temp); 728 + const feels = Math.round(data.main?.feels_like); 729 + const cond = data.weather?.[0]?.description || 'Unknown'; 730 + const hum = data.main?.humidity; 731 + const wind = Math.round(data.wind?.speed); 732 + const pres = data.main?.pressure; 733 + await target.reply( 734 + `Weather for ${data.name || location}: ${temp}°F (feels ${feels}°F), ${cond}. Humidity ${hum}%, Wind ${wind} mph, Pressure ${pres} hPa.`, 735 + ); 736 + executed.push({ name, success: true }); 737 + } 738 + } 739 + } 740 + } catch (err) { 741 + logger.error('Error executing weather tool:', { err }); 742 + executed.push({ name, success: false }); 743 + } 744 + } else if (name === 'wiki') { 745 + try { 746 + const query = (tc.args?.search as string) || (tc.args?.query as string) || ''; 747 + const q = typeof query === 'string' ? query.trim() : ''; 748 + if (!q) { 749 + executed.push({ name, success: false }); 750 + } else { 751 + const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(q)}`; 752 + const res = await fetch(url); 753 + if (!res.ok) { 754 + executed.push({ name, success: false }); 755 + } else { 756 + const data = await res.json(); 757 + const title = data.title || q; 758 + const extract = data.extract || 'No summary available.'; 759 + const pageUrl = 760 + data.content_urls?.desktop?.page || 761 + `https://en.wikipedia.org/wiki/${encodeURIComponent(q)}`; 762 + await target.reply(`${title}\n${extract}\n<${pageUrl}>`); 763 + executed.push({ name, success: true }); 764 + } 765 + } 766 + } catch (err) { 767 + logger.error('Error executing wiki tool:', { err }); 768 + executed.push({ name, success: false }); 769 + } 770 + } 771 + } 772 + 773 + if (onlyReactions) { 774 + const anyFailed = executed.some((e) => e.name === 'reaction' && !e.success); 775 + if (anyFailed && sentMessage) { 776 + try { 777 + await sentMessage.edit( 778 + 'I tried to react, but I do not have permission to add reactions here or the emoji was invalid.', 779 + ); 780 + } catch (e) { 781 + logger.error('Failed to edit placeholder message after reaction failure:', e); 782 + } 783 + } 784 + } 785 + } 392 786 393 787 const userMessage: ConversationMessage = { 394 788 role: 'user', ··· 413 807 414 808 logger.info(`${isDM ? 'DM' : 'Server'} response sent successfully`); 415 809 } catch (error) { 416 - logger.error( 417 - `Error processing ${isDM ? 'DM' : 'server message'}:`, 418 - error instanceof Error ? error.message : String(error), 419 - ); 810 + const err = error as Error; 811 + logger.error(`Error processing ${isDM ? 'DM' : 'server message'}:`, { 812 + message: err?.message, 813 + stack: err?.stack, 814 + raw: error, 815 + }); 420 816 try { 421 817 await message.reply( 422 818 'Sorry, I encountered an error processing your message. Please try again later.', ··· 427 823 } 428 824 } 429 825 430 - private async sendResponse(message: Message, aiResponse: AIResponse): Promise<void> { 826 + private async sendResponse(message: Message, aiResponse: AIResponse): Promise<Message | void> { 431 827 let fullResponse = ''; 432 828 433 829 if (aiResponse.reasoning) { ··· 436 832 437 833 fullResponse += aiResponse.content; 438 834 835 + if (!fullResponse || !fullResponse.trim()) { 836 + fullResponse = '\u200b'; 837 + } 838 + 439 839 const maxLength = 2000; 440 840 if (fullResponse.length <= maxLength) { 441 - await message.reply({ 841 + const sent = await message.reply({ 442 842 content: fullResponse, 443 843 allowedMentions: { parse: ['users'] as const }, 444 844 }); 445 - } else { 446 - const chunks = splitResponseIntoChunks(fullResponse, maxLength); 845 + return sent; 846 + } 847 + const chunks = splitResponseIntoChunks(fullResponse, maxLength); 447 848 448 - await message.reply({ content: chunks[0], allowedMentions: { parse: ['users'] as const } }); 849 + const first = await message.reply({ 850 + content: chunks[0], 851 + allowedMentions: { parse: ['users'] as const }, 852 + }); 449 853 450 - for (let i = 1; i < chunks.length; i++) { 451 - if ('send' in message.channel) { 452 - await message.channel.send({ 453 - content: chunks[i], 454 - allowedMentions: { parse: ['users'] as const }, 455 - }); 456 - } 854 + for (let i = 1; i < chunks.length; i++) { 855 + if ('send' in message.channel) { 856 + await message.channel.send({ 857 + content: chunks[i], 858 + allowedMentions: { parse: ['users'] as const }, 859 + }); 457 860 } 458 861 } 862 + return first; 459 863 } 460 864 }
+1 -1
src/events/ready.ts
··· 4 4 5 5 export default class ReadyEvent { 6 6 constructor(c: BotClient) { 7 - c.once('ready', () => this.readyEvent(c)); 7 + c.once('clientReady', () => this.readyEvent(c)); 8 8 } 9 9 10 10 private async readyEvent(client: BotClient) {
+1
src/index.ts
··· 5 5 import path from 'path'; 6 6 import { fileURLToPath } from 'url'; 7 7 8 + import './utils/djsModalPatch'; 8 9 import BotClient from './services/Client'; 9 10 import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './config'; 10 11 import rateLimit from 'express-rate-limit';
+1 -1
src/routes/apiKeys.ts
··· 13 13 'api.anthropic.com', 14 14 'api.deepseek.com', 15 15 'api.moonshot.ai', 16 - 'api.perplexity.ai' 16 + 'api.perplexity.ai', 17 17 ]; 18 18 19 19 function getOpenAIClient(apiKey: string, baseURL?: string): OpenAI {
+38
src/services/Client.ts
··· 21 21 // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 22 public t = new Collection<string, any>(); 23 23 public socialMediaManager?: SocialMediaManager; 24 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 + public lastModalRawByUser: Map<string, any> = new Map(); 24 26 25 27 constructor() { 26 28 super({ ··· 41 43 }, 42 44 }); 43 45 BotClient.instance = this; 46 + 47 + interface RawPacket { 48 + t: string; 49 + d?: { 50 + type: number; 51 + member?: { 52 + user: { 53 + id: string; 54 + }; 55 + }; 56 + user?: { 57 + id: string; 58 + }; 59 + }; 60 + } 61 + 62 + this.on('raw', (packet: RawPacket) => { 63 + try { 64 + if (packet?.t !== 'INTERACTION_CREATE') return; 65 + const d = packet.d; 66 + if (d?.type !== 5) return; 67 + const userId = d?.member?.user?.id || d?.user?.id; 68 + if (!userId) return; 69 + this.lastModalRawByUser.set(userId, d); 70 + setTimeout( 71 + () => { 72 + if (this.lastModalRawByUser.get(userId) === d) { 73 + this.lastModalRawByUser.delete(userId); 74 + } 75 + }, 76 + 5 * 60 * 1000, 77 + ); 78 + } catch (e) { 79 + logger.warn('Failed to capture raw modal payload', e); 80 + } 81 + }); 44 82 } 45 83 46 84 public static getInstance(): BotClient | null {
+222
src/types/componentsV2.ts
··· 1 + export type V2ComponentType = 3 | 4 | 10 | 12 | 18; 2 + 3 + export interface V2BaseComponent { 4 + type: V2ComponentType; 5 + } 6 + 7 + export interface V2StringSelect extends V2BaseComponent { 8 + type: 3; 9 + custom_id: string; 10 + placeholder?: string; 11 + options: Array<{ 12 + label: string; 13 + value: string; 14 + description?: string; 15 + emoji?: { id?: string; name?: string; animated?: boolean }; 16 + }>; 17 + min_values?: number; 18 + max_values?: number; 19 + } 20 + 21 + export interface V2TextInput extends V2BaseComponent { 22 + type: 4; 23 + custom_id: string; 24 + style: 1 | 2; 25 + label?: string; 26 + placeholder?: string; 27 + required?: boolean; 28 + min_length?: number; 29 + max_length?: number; 30 + value?: string; 31 + } 32 + 33 + export interface V2LabeledComponent extends V2BaseComponent { 34 + type: 18; 35 + label?: string; 36 + description?: string; 37 + component: V2StringSelect | V2TextInput; 38 + } 39 + 40 + export type V2ModalRow = 41 + | V2LabeledComponent 42 + | { type: 1; components: V2TextInput[] } 43 + | { type: 18; components: V2TextInput[] }; 44 + 45 + export interface V2SubmissionValueMap { 46 + [customId: string]: string | string[]; 47 + } 48 + 49 + export interface V2ModalPayload { 50 + custom_id: string; 51 + title: string; 52 + components: V2ModalRow[]; 53 + } 54 + 55 + interface V2Component { 56 + type: number; 57 + custom_id?: string; 58 + customId?: string; 59 + value?: string | string[]; 60 + values?: string[]; 61 + [key: string]: unknown; 62 + } 63 + 64 + interface V2Row { 65 + component?: V2Component; 66 + components?: V2Component[]; 67 + type?: number; 68 + [key: string]: unknown; 69 + } 70 + 71 + export function buildProviderModal(customId: string, title: string): V2ModalPayload { 72 + return { 73 + custom_id: customId, 74 + title, 75 + components: [ 76 + { 77 + type: 18, 78 + label: 'AI Provider', 79 + description: 'Select an authorized provider', 80 + component: { 81 + type: 3, 82 + custom_id: 'provider', 83 + placeholder: 'Choose provider', 84 + options: [ 85 + { label: 'OpenAI', value: 'openai', description: 'api.openai.com' }, 86 + { label: 'OpenRouter', value: 'openrouter', description: 'openrouter.ai' }, 87 + { 88 + label: 'Google Gemini', 89 + value: 'gemini', 90 + description: 'generativelanguage.googleapis.com', 91 + }, 92 + { label: 'DeepSeek', value: 'deepseek', description: 'api.deepseek.com' }, 93 + { label: 'Moonshot AI', value: 'moonshot', description: 'api.moonshot.ai' }, 94 + { label: 'Perplexity AI', value: 'perplexity', description: 'api.perplexity.ai' }, 95 + ], 96 + }, 97 + }, 98 + { 99 + type: 1 as const, 100 + components: [ 101 + { 102 + type: 4, 103 + custom_id: 'model', 104 + label: 'Model', 105 + style: 1, 106 + required: true, 107 + placeholder: 'openai/gpt-4o-mini', 108 + min_length: 2, 109 + max_length: 100, 110 + }, 111 + ], 112 + }, 113 + { 114 + type: 1 as const, 115 + components: [ 116 + { 117 + type: 4, 118 + custom_id: 'apiKey', 119 + label: 'API Key', 120 + style: 1, 121 + required: true, 122 + placeholder: 'sk-... or other', 123 + min_length: 10, 124 + max_length: 500, 125 + }, 126 + ], 127 + }, 128 + ], 129 + }; 130 + } 131 + 132 + interface RawModalSubmission { 133 + fields?: { 134 + getTextInputValue?: (id: string) => string; 135 + [key: string]: unknown; 136 + }; 137 + data?: { 138 + components?: Array<{ 139 + component?: V2Component; 140 + components?: V2Component[]; 141 + [key: string]: unknown; 142 + }>; 143 + [key: string]: unknown; 144 + }; 145 + components?: Array<{ 146 + component?: V2Component; 147 + components?: V2Component[]; 148 + [key: string]: unknown; 149 + }>; 150 + message?: { 151 + components?: Array<{ 152 + component?: V2Component; 153 + components?: V2Component[]; 154 + [key: string]: unknown; 155 + }>; 156 + [key: string]: unknown; 157 + }; 158 + [key: string]: unknown; 159 + } 160 + 161 + export function parseV2ModalSubmission(raw: RawModalSubmission): V2SubmissionValueMap { 162 + const result: V2SubmissionValueMap = {}; 163 + try { 164 + const fields = raw?.fields as { getTextInputValue?: (id: string) => string } | undefined; 165 + if (fields?.getTextInputValue) { 166 + for (const id of ['model', 'apiKey']) { 167 + try { 168 + const v = fields.getTextInputValue(id); 169 + if (v !== undefined && v !== '') result[id] = v; 170 + } catch (error) { 171 + console.error(`Error getting text input value for ${id}:`, error); 172 + } 173 + } 174 + } 175 + } catch (error) { 176 + console.error('Error processing text input fields:', error); 177 + } 178 + 179 + try { 180 + const mergedRows = ( 181 + [ 182 + ...(raw?.data?.components || []), 183 + ...(raw?.components || []), 184 + ...(raw?.message?.components || []), 185 + ] as Array<V2Row | undefined> 186 + ).filter(Boolean) as V2Row[]; 187 + 188 + const flat = mergedRows.flatMap((r) => (r.component ? [r.component] : r.components || [])); 189 + 190 + for (const c of flat) { 191 + if (!c) continue; 192 + 193 + if (c.customId === 'provider' || c.custom_id === 'provider') { 194 + if (Array.isArray((c as { values?: string[] }).values)) { 195 + result.provider = (c as { values: string[] }).values; 196 + } else if ((c as { value?: string | string[] }).value) { 197 + const value = (c as { value: string | string[] }).value; 198 + result.provider = Array.isArray(value) ? value : [value]; 199 + } 200 + } 201 + 202 + if (c.type === 4 && (c.custom_id || c.customId) && (c as { value?: unknown }).value) { 203 + const value = (c as { value: unknown }).value; 204 + if (typeof value === 'string') { 205 + result[c.custom_id || c.customId!] = value; 206 + } 207 + } 208 + } 209 + } catch (error) { 210 + console.error('Error processing component values:', error); 211 + } 212 + return result; 213 + } 214 + 215 + export const PROVIDER_TO_URL: Record<string, string> = { 216 + openai: 'https://api.openai.com/v1', 217 + openrouter: 'https://openrouter.ai/api/v1', 218 + gemini: 'https://generativelanguage.googleapis.com', 219 + deepseek: 'https://api.deepseek.com', 220 + moonshot: 'https://api.moonshot.ai', 221 + perplexity: 'https://api.perplexity.ai', 222 + };
+692
src/utils/commandExecutor.ts
··· 1 + import { 2 + ChatInputCommandInteraction, 3 + InteractionReplyOptions, 4 + MessagePayload, 5 + EmbedBuilder, 6 + } from 'discord.js'; 7 + import { SlashCommandProps } from '@/types/command'; 8 + import BotClient from '@/services/Client'; 9 + import logger from '@/utils/logger'; 10 + 11 + export interface ToolCall { 12 + name: string; 13 + args: Record<string, unknown>; 14 + } 15 + 16 + type 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 + 27 + function 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 + 52 + function 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 + 77 + export 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 + 133 + export 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 + }
+77
src/utils/djsModalPatch.ts
··· 1 + import Module from 'module'; 2 + import path from 'path'; 3 + 4 + class SafeModalSubmitFields { 5 + public components: unknown[] = []; 6 + 7 + public fields: { 8 + getField: (customId: string) => { value: string }; 9 + }; 10 + 11 + private fieldsMap: Record<string, string> = {}; 12 + 13 + constructor(components: unknown) { 14 + this.components = Array.isArray(components) ? components : []; 15 + 16 + try { 17 + const rows = this.components as Array<Record<string, unknown>>; 18 + for (const row of rows) { 19 + const labeled = row?.component ? [row.component] : undefined; 20 + const innerComponents = Array.isArray(row?.components) ? row.components : labeled || []; 21 + 22 + for (const comp of innerComponents) { 23 + const leaf = comp?.component || comp; 24 + const isTextInput = leaf?.type === 4 || typeof leaf?.style === 'number'; 25 + if (!isTextInput) continue; 26 + const id = leaf?.custom_id || leaf?.customId || leaf?.id; 27 + if (!id) continue; 28 + const value = typeof leaf?.value === 'string' ? leaf.value : ''; 29 + this.fieldsMap[id] = value; 30 + } 31 + } 32 + } catch { 33 + // ignore 34 + } 35 + 36 + this.fields = { 37 + getField: (customId: string) => { 38 + if (!(customId in this.fieldsMap)) { 39 + throw new Error(`Text input with custom id '${customId}' not found`); 40 + } 41 + return { value: this.fieldsMap[customId] }; 42 + }, 43 + }; 44 + } 45 + 46 + getTextInputValue(customId: string): string { 47 + return this.fields.getField(customId).value; 48 + } 49 + } 50 + 51 + try { 52 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 + const anyModule = Module as any; 54 + const originalLoad = anyModule._load; 55 + anyModule._load = function ( 56 + this: unknown, 57 + request: string, 58 + parent: unknown, 59 + _isMain: boolean, 60 + ...args: unknown[] 61 + ) { 62 + try { 63 + const resolved = anyModule._resolveFilename(request, parent); 64 + if (typeof resolved === 'string') { 65 + const normalized = path.posix.normalize(resolved.replace(/\\/g, '/')); 66 + if (normalized.includes('/structures/ModalSubmitFields.js')) { 67 + return SafeModalSubmitFields; 68 + } 69 + } 70 + } catch { 71 + // fallthrough 72 + } 73 + return originalLoad.apply(this, [request, parent, _isMain, ...args] as const); 74 + }; 75 + } catch { 76 + // ignore 77 + }
+124
src/utils/messageToolExecutor.ts
··· 1 + import { Message } from 'discord.js'; 2 + import BotClient from '@/services/Client'; 3 + import logger from '@/utils/logger'; 4 + 5 + export interface MessageToolCall { 6 + name: string; 7 + args: Record<string, unknown>; 8 + } 9 + 10 + function extractEmojiArg(args: Record<string, unknown>): string { 11 + const raw = (args.emoji as string) || (args.query as string) || ''; 12 + return typeof raw === 'string' ? raw.trim() : ''; 13 + } 14 + 15 + function normalizeEmoji(input: string): string { 16 + if (!input) return ''; 17 + const customMatch = input.match(/<a?:\w+:(\d+)>/); 18 + if (customMatch && customMatch[1]) { 19 + return customMatch[0]; 20 + } 21 + const shortcode = input.match(/^:([a-z0-9_+-]+):$/i)?.[1]; 22 + if (shortcode) { 23 + const map: Record<string, string> = { 24 + thumbsup: '👍', 25 + thumbsdown: '👎', 26 + '+1': '👍', 27 + '-1': '👎', 28 + thumbs_up: '👍', 29 + thumbs_down: '👎', 30 + heart: '❤️', 31 + smile: '😄', 32 + grin: '😁', 33 + joy: '😂', 34 + cry: '😢', 35 + sob: '😭', 36 + clap: '👏', 37 + fire: '🔥', 38 + star: '⭐', 39 + eyes: '👀', 40 + tada: '🎉', 41 + }; 42 + const key = shortcode.toLowerCase(); 43 + return map[key] || input; 44 + } 45 + return input; 46 + } 47 + 48 + export async function executeMessageToolCall( 49 + toolCall: MessageToolCall, 50 + message: Message, 51 + _client: BotClient, 52 + opts?: { originalMessage?: Message; botMessage?: Message }, 53 + ): Promise<{ success: boolean; type: string; handled: boolean; error?: string }> { 54 + const name = (toolCall.name || '').toLowerCase(); 55 + try { 56 + if (name === 'reaction') { 57 + const emojiRaw = extractEmojiArg(toolCall.args || {}); 58 + const emoji = normalizeEmoji(emojiRaw); 59 + if (!emoji) { 60 + return { success: false, type: 'reaction', handled: false, error: 'Missing emoji' }; 61 + } 62 + 63 + let targetMsg: Message = message; 64 + try { 65 + const targetSpec = (toolCall.args?.target as string) || ''; 66 + const msgId = (toolCall.args?.target_message_id as string) || ''; 67 + const url = (toolCall.args?.target_url as string) || ''; 68 + 69 + if (targetSpec === 'user' && opts?.originalMessage) { 70 + targetMsg = opts.originalMessage; 71 + } else if (targetSpec === 'bot' && opts?.botMessage) { 72 + targetMsg = opts.botMessage; 73 + } else if (msgId && message.channel && 'messages' in message.channel) { 74 + try { 75 + const fetched = await message.channel.messages.fetch(msgId); 76 + if (fetched) targetMsg = fetched; 77 + } catch (error) { 78 + logger.warn(`Failed to fetch message with ID ${msgId}:`, error); 79 + } 80 + } else if (url) { 81 + const m = url.match(/discord\.com\/channels\/\d+\/(\d+)\/(\d+)/); 82 + const channelId = m?.[1]; 83 + const messageId = m?.[2]; 84 + try { 85 + if (channelId && messageId && message.client.channels) { 86 + const ch = await message.client.channels.fetch(channelId); 87 + if (ch && 'messages' in ch) { 88 + const fetched = await ch.messages.fetch(messageId); 89 + if (fetched) targetMsg = fetched; 90 + } 91 + } 92 + } catch (error) { 93 + logger.warn(`Failed to fetch message from URL ${url}:`, error); 94 + } 95 + } 96 + } catch (error) { 97 + logger.warn('Error processing message target:', error); 98 + } 99 + 100 + try { 101 + await targetMsg.react(emoji); 102 + return { success: true, type: 'reaction', handled: true }; 103 + } catch (_err) { 104 + try { 105 + await targetMsg.react(emojiRaw); 106 + return { success: true, type: 'reaction', handled: true }; 107 + } catch (error) { 108 + const errorMessage = error instanceof Error ? error.message : String(error); 109 + logger.error('[MessageToolExecutor] Failed to add reaction:', { 110 + emoji: emojiRaw, 111 + error: errorMessage, 112 + }); 113 + return { success: false, type: 'reaction', handled: false, error: errorMessage }; 114 + } 115 + } 116 + } 117 + 118 + return { success: false, type: name || 'unknown', handled: false, error: 'Unknown tool' }; 119 + } catch (error) { 120 + const errorMessage = error instanceof Error ? error.message : String(error); 121 + logger.error('[MessageToolExecutor] Error executing tool call:', { name, error: errorMessage }); 122 + return { success: false, type: name || 'unknown', handled: false, error: errorMessage }; 123 + } 124 + }