diff --git a/bun.lock b/bun.lock index 4a6f6c3..7fb067a 100644 --- a/bun.lock +++ b/bun.lock @@ -4,15 +4,15 @@ "": { "name": "aethel", "dependencies": { - "@atproto/identity": "^0.4.8", - "@discordjs/rest": "^2.5.1", - "@fedify/fedify": "^1.1.0", + "@atproto/identity": "^0.4.9", + "@discordjs/rest": "^2.6.0", + "@fedify/fedify": "^1.8.12", "@types/he": "^1.2.3", "@types/sanitize-html": "^2.16.0", - "axios": "^1.11.0", - "city-timezones": "^1.3.1", + "axios": "^1.12.2", + "city-timezones": "^1.3.2", "cors": "^2.8.5", - "discord.js": "^14.21.0", + "discord.js": "^14.22.1", "dotenv": "^16.6.1", "eslint-plugin-prettier": "^5.5.4", "express": "^4.21.2", @@ -24,7 +24,7 @@ "moment-timezone": "^0.6.0", "node-fetch": "^3.3.2", "open-graph-scraper": "^6.10.0", - "openai": "^5.12.2", + "openai": "^5.23.1", "pg": "^8.16.3", "sanitize-html": "^2.17.0", "uuid": "^11.1.0", @@ -33,35 +33,35 @@ "winston": "^3.17.0", }, "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.36.0", "@types/cors": "^2.8.19", "@types/express": "^4.17.23", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^24.2.1", + "@types/node": "^24.5.2", "@types/open-graph-scraper": "^5.2.3", "@types/pg": "^8.15.5", "@types/uuid": "^10.0.0", - "@types/validator": "^13.15.2", + "@types/validator": "^13.15.3", "@types/whois-json": "^2.0.4", - "eslint": "^9.33.0", + "eslint": "^9.36.0", "eslint-config-prettier": "^10.1.8", - "globals": "^16.3.0", + "globals": "^16.4.0", "nodemon": "^3.1.10", "prettier": "^3.6.2", "tsc-alias": "^1.8.16", "tsconfig-paths": "^4.2.0", - "tsx": "^4.20.3", + "tsx": "^4.20.6", "typescript": "^5.9.2", - "typescript-eslint": "^8.39.0", + "typescript-eslint": "^8.44.1", }, }, }, "packages": { - "@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=="], + "@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=="], "@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=="], - "@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=="], + "@atproto/identity": ["@atproto/identity@0.4.9", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/crypto": "^0.4.4" } }, "sha512-pRYCaeaEJMZ4vQlRQYYTrF3cMiRp21n/k/pUT1o7dgKby56zuLErDmFXkbKfKWPf7SgWRgamSaNmsGLqAOD7lQ=="], "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], @@ -77,7 +77,7 @@ "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], - "@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=="], + "@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=="], "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], @@ -135,7 +135,7 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], - "@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=="], + "@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=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], @@ -147,7 +147,7 @@ "@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=="], - "@eslint/js": ["@eslint/js@9.34.0", "", {}, "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw=="], + "@eslint/js": ["@eslint/js@9.36.0", "", {}, "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], @@ -155,7 +155,7 @@ "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], - "@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=="], + "@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=="], "@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=="], @@ -223,7 +223,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], "@types/open-graph-scraper": ["@types/open-graph-scraper@5.2.3", "", { "dependencies": { "open-graph-scraper": "*" } }, "sha512-R6ew1HJndBKsys2+Y10VW8yy3ojS7eF/mFXrOZSFxVqY7WI4ubxaFvgfaULnRn2pq149SpS2GZNB9i9Y5fQqEw=="], @@ -243,31 +243,31 @@ "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], - "@types/validator": ["@types/validator@13.15.2", "", {}, "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q=="], + "@types/validator": ["@types/validator@13.15.3", "", {}, "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q=="], "@types/whois-json": ["@types/whois-json@2.0.4", "", {}, "sha512-Pp5N/+A6LUE0FWXz6wQ2gV5wEw0uEqFBeSLuQAGdeTyRJv/bbz7PPj3H78jyulvQu7cnMpXTzKx4bo8TuPAYhw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], - "@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=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ=="], - "@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=="], + "@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=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], @@ -299,7 +299,7 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "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=="], + "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=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -345,7 +345,7 @@ "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=="], - "city-timezones": ["city-timezones@1.3.1", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-YCeJKGyw3DA+wV/oyuFuJlk4oqN9zkfLP+fz2nEXUBm9sW1xZaXQsKQoc8l8hP+vI45GPOq8OuGrlGXUcnLISA=="], + "city-timezones": ["city-timezones@1.3.2", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-XztdL/2EWpfmgRIOzrKVOWFp6VdmaD9FNTZPINlez1etIn0mMNn01RMmSfOp6LUP/h1M2ZLX80N1O+WKwhzC+w=="], "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -405,7 +405,7 @@ "discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], - "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=="], + "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=="], "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=="], @@ -451,7 +451,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "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=="], + "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=="], "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=="], @@ -535,7 +535,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], "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=="], @@ -729,7 +729,7 @@ "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=="], - "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=="], + "openai": ["openai@5.23.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-APxMtm5mln4jhKhAr0d5zP9lNsClx4QwJtg8RUvYSSyxYCTHLNJnLEcSHbJ6t0ori8Pbr9HZGfcPJ7LEy73rvQ=="], "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=="], @@ -941,7 +941,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "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=="], + "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=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -949,7 +949,7 @@ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "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=="], + "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=="], "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], @@ -959,7 +959,7 @@ "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -1027,6 +1027,8 @@ "@digitalbazaar/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@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=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -1089,6 +1091,24 @@ "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "@types/body-parser/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/connect/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/cors/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/jsonwebtoken/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/send/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/serve-static/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/ws/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], diff --git a/package.json b/package.json index 0fccebd..c2aeeaf 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,15 @@ "check": "pnpm run lint && pnpm run format:check" }, "dependencies": { - "@atproto/identity": "^0.4.8", - "@discordjs/rest": "^2.5.1", - "@fedify/fedify": "^1.1.0", + "@atproto/identity": "^0.4.9", + "@discordjs/rest": "^2.6.0", + "@fedify/fedify": "^1.8.12", "@types/he": "^1.2.3", "@types/sanitize-html": "^2.16.0", - "axios": "^1.11.0", - "city-timezones": "^1.3.1", + "axios": "^1.12.2", + "city-timezones": "^1.3.2", "cors": "^2.8.5", - "discord.js": "^14.21.0", + "discord.js": "^14.22.1", "dotenv": "^16.6.1", "eslint-plugin-prettier": "^5.5.4", "express": "^4.21.2", @@ -38,7 +38,7 @@ "moment-timezone": "^0.6.0", "node-fetch": "^3.3.2", "open-graph-scraper": "^6.10.0", - "openai": "^5.12.2", + "openai": "^5.23.1", "pg": "^8.16.3", "sanitize-html": "^2.17.0", "uuid": "^11.1.0", @@ -47,25 +47,25 @@ "winston": "^3.17.0" }, "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.36.0", "@types/cors": "^2.8.19", "@types/express": "^4.17.23", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^24.2.1", + "@types/node": "^24.5.2", "@types/open-graph-scraper": "^5.2.3", "@types/pg": "^8.15.5", "@types/uuid": "^10.0.0", - "@types/validator": "^13.15.2", + "@types/validator": "^13.15.3", "@types/whois-json": "^2.0.4", - "eslint": "^9.33.0", + "eslint": "^9.36.0", "eslint-config-prettier": "^10.1.8", - "globals": "^16.3.0", + "globals": "^16.4.0", "nodemon": "^3.1.10", "prettier": "^3.6.2", "tsc-alias": "^1.8.16", "tsconfig-paths": "^4.2.0", - "tsx": "^4.20.3", + "tsx": "^4.20.6", "typescript": "^5.9.2", - "typescript-eslint": "^8.39.0" + "typescript-eslint": "^8.44.1" } } diff --git a/src/commands/fun/cat.ts b/src/commands/fun/cat.ts index 45ea7e2..2915d67 100644 --- a/src/commands/fun/cat.ts +++ b/src/commands/fun/cat.ts @@ -29,7 +29,7 @@ const cooldownManager = createCooldownManager('cat', 3000); const commandLogger = createCommandLogger('cat'); const errorHandler = createErrorHandler('cat'); -async function fetchCatImage(): Promise { +export async function fetchCatImage(): Promise { const response = await fetch('https://api.pur.cat/random-cat'); //cat if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); diff --git a/src/commands/fun/dog.ts b/src/commands/fun/dog.ts index ec6edd2..28211d9 100644 --- a/src/commands/fun/dog.ts +++ b/src/commands/fun/dog.ts @@ -31,12 +31,13 @@ const cooldownManager = createCooldownManager('dog', 3000); const commandLogger = createCommandLogger('dog'); const errorHandler = createErrorHandler('dog'); -async function fetchDogImage(): Promise { +export async function fetchDogImage(): Promise { const response = await fetch('https://api.erm.dog/random-dog', { headers: browserHeaders }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); } - return (await response.json()) as RandomReddit; + const data = (await response.json()) as RandomReddit; + return data; } export default { @@ -58,7 +59,7 @@ export default { ]) .setIntegrationTypes(ApplicationIntegrationType.UserInstall), - async execute(client, interaction) { + execute: async (client, interaction) => { try { const cooldownCheck = await checkCooldown( cooldownManager, @@ -110,10 +111,15 @@ export default { ), ); - await interaction.editReply({ - components: [container], - flags: MessageFlags.IsComponentsV2, - }); + try { + await interaction.editReply({ + components: [container], + flags: MessageFlags.IsComponentsV2, + }); + } catch (replyError) { + logger.error('Failed to send reply:', replyError); + throw replyError; + } } catch (error) { await errorHandler({ interaction, diff --git a/src/commands/utilities/ai.ts b/src/commands/utilities/ai.ts index 41659c4..fb4658e 100644 --- a/src/commands/utilities/ai.ts +++ b/src/commands/utilities/ai.ts @@ -1,26 +1,56 @@ +import type { ToolCall } from '@/utils/commandExecutor'; +import { extractToolCalls, executeToolCall } from '@/utils/commandExecutor'; +import BotClient from '@/services/Client'; +import { + buildProviderModal, + PROVIDER_TO_URL, + parseV2ModalSubmission, + type V2ModalPayload, +} from '@/types/componentsV2'; import { SlashCommandBuilder, + SlashCommandOptionsOnlyBuilder, + EmbedBuilder, ModalBuilder, TextInputBuilder, - TextInputStyle, ActionRowBuilder, + TextInputStyle, + StringSelectMenuInteraction, ModalSubmitInteraction, ChatInputCommandInteraction, + InteractionResponse, + Message, InteractionContextType, ApplicationIntegrationType, MessageFlags, } from 'discord.js'; import OpenAI from 'openai'; +import fetch from '@/utils/dynamicFetch'; import pool from '@/utils/pgClient'; import { encrypt, decrypt, isValidEncryptedFormat, EncryptionError } from '@/utils/encrypt'; import { SlashCommandProps } from '@/types/command'; -import BotClient from '@/services/Client'; import logger from '@/utils/logger'; import { createCommandLogger } from '@/utils/commandLogger'; import { createErrorHandler } from '@/utils/errorHandler'; import { createMemoryManager } from '@/utils/memoryManager'; -const ALLOWED_API_HOSTS = ['api.openai.com', 'openrouter.ai', 'generativelanguage.googleapis.com']; +function getInvokerId( + interaction: ChatInputCommandInteraction | ModalSubmitInteraction | StringSelectMenuInteraction, +): string { + if ('guildId' in interaction && interaction.guildId) { + return `${interaction.guildId}-${interaction.user.id}`; + } + return interaction.user.id; +} + +const ALLOWED_API_HOSTS = [ + 'api.openai.com', + 'openrouter.ai', + 'generativelanguage.googleapis.com', + 'api.deepseek.com', + 'api.moonshot.ai', + 'api.perplexity.ai', +]; interface ConversationMessage { role: 'system' | 'user' | 'assistant'; @@ -40,6 +70,8 @@ interface ConversationMessage { interface AIResponse { content: string; reasoning?: string; + toolResults?: string; + citations?: string[]; } interface OpenAIMessageWithReasoning { @@ -50,15 +82,43 @@ interface OpenAIMessageWithReasoning { interface PendingRequest { interaction: ChatInputCommandInteraction; prompt: string; - timestamp: number; + createdAt: number; + status?: 'awaiting' | 'processing'; } +type ModalRawEntry = { data?: { components?: unknown[] }; components?: unknown[] }; +type ClientWithModalState = BotClient & { lastModalRawByUser?: Map }; + interface UserCredentials { apiKey?: string | null; model?: string | null; apiUrl?: string | null; } +async function showV2Modal( + interaction: ChatInputCommandInteraction | StringSelectMenuInteraction, + v2Modal: V2ModalPayload, +): Promise { + const url = `https://discord.com/api/v10/interactions/${interaction.id}/${interaction.token}/callback?with_response=false`; + const body = { + type: 9, + data: { + custom_id: v2Modal.custom_id, + title: v2Modal.title, + components: v2Modal.components, + }, + }; + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!resp.ok) { + const text = await resp.text(); + throw new Error(`Discord API error showing modal: ${resp.status} ${text}`); + } +} + interface User { user_id: string; api_key_encrypted?: string; @@ -156,16 +216,14 @@ function buildSystemPrompt( hour: '2-digit', minute: '2-digit', second: '2-digit', - hour12: true, timeZone: timezone, }); - let supportedCommands = '/help - Show all available commands and their usage'; + const supportedCommands = '/help - Show all available commands and their usage'; if (client?.commands) { - const commandEntries = Array.from(client.commands.entries()).sort(([a], [b]) => - a.localeCompare(b), - ); - supportedCommands = commandEntries + const commandEntries = Array.from(client.commands.entries()); + commandEntries.sort((a, b) => a[0].localeCompare(b[0])); + const _commandList = commandEntries .map( ([name, command]) => `/${name} - ${command.data.description || 'No description available'}`, ) @@ -208,10 +266,11 @@ ${contextInfo} - Timezone: ${timezone} **IMPORTANT INSTRUCTIONS:** +- 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. - NEVER format, modify, or alter URLs in any way. Leave them exactly as they are. - Format your responses using Discord markdown where appropriate, but NEVER format URLs. - Only greet the user at the start of a new conversation, not in every message. -- DO NOT hallucinate, make up facts, or provide false information. If you don't know something, say so clearly. +- 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. - Be accurate and truthful in all responses. Do not invent details, statistics, or information that you're not certain about. - If asked about current events, real-time data, or information beyond your knowledge cutoff, clearly state your limitations. @@ -221,7 +280,30 @@ ${contextInfo} - Developer: scanash (main maintainer) and Aethel Labs (org) - Open source: https://github.com/Aethel-Labs/aethel - Type: Discord user bot -- Supported commands: ${supportedCommands}`; +- Supported commands: ${supportedCommands} + +**TOOL USAGE:** +You can use tools by placing commands in {curly braces}. Available tools: +- {cat:} - Get a cat picture, if user asks for a cat picture, use this tool. +- {dog:} - Get a dog picture, if user asks for a dog picture, use this tool. +- {joke: or {joke: {type: "general/knock-knock/programming/dad"}} } - Get a joke +- {weather:{"location":"city"}} - Check weather, use if user asks for weather. +- {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. +- {reaction:"πŸ˜€"} - React to the user's message with a unicode emoji +- {reaction:{"emoji":":thumbsup:"}} - React using a named emoji if available +- {reaction:{"emoji":"<:name:123456789012345678>"}} - React with a custom emoji by ID (or animated ) + +Use the wikipedia search when you want to look for information outside of your knowledge, state it came from Wikipedia if used. + +**REACTION GUIDELINES:** +- When asked to react, ALWAYS use the {reaction:"emoji"} tool call +- Use reactions sparingly and only when it adds value to the conversation +- Add at most 1–2 reactions for a single message +- Do not include the reaction tool call text in your visible reply +- Common reactions: πŸ˜€ πŸ˜„ πŸ‘ πŸ‘Ž ❀️ πŸ”₯ ⭐ πŸŽ‰ πŸ‘ +- Example: If asked to react with thumbs up, use {reaction:"πŸ‘"} and respond normally + +When you use a tool, you'll receive a JSON response with the command results.`; const modelSpecificInstructions = usingDefaultKey ? '\n\n**IMPORTANT:** Please keep your responses under 3000 characters. Be concise and to the point.' @@ -413,19 +495,73 @@ async function testApiKey( apiUrl: string, ): Promise<{ success: boolean; error?: string }> { try { - const client = getOpenAIClient(apiKey, apiUrl); - - await client.chat.completions.create({ - model, - messages: [ - { - role: 'user', - content: 'Hello! This is a test message. Please respond with "API key test successful!"', + const url = new URL(apiUrl); + const host = url.hostname; + + if (host === 'generativelanguage.googleapis.com') { + const base = apiUrl.replace(/\/$/, ''); + const mdl = model.startsWith('models/') ? model : `models/${model}`; + const endpoint = `${base}/v1beta/${mdl}:generateContent?key=${encodeURIComponent(apiKey)}`; + + const resp = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [ + { + parts: [ + { + text: 'Hello! This is a test message. Please respond with "API key test successful!"', + }, + ], + }, + ], + }), + }); + + if (!resp.ok) { + const text = await resp.text(); + throw new Error(`${resp.status} ${text || resp.statusText}`); + } + } else if (host === 'api.perplexity.ai') { + const resp = await fetch(`${url.origin}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, }, - ], - max_tokens: 50, - temperature: 0.1, - }); + body: JSON.stringify({ + model, + messages: [ + { + role: 'user', + content: + 'Hello! This is a test message. Please respond with "API key test successful!"', + }, + ], + max_tokens: 50, + temperature: 0.1, + }), + }); + if (!resp.ok) { + const text = await resp.text(); + throw new Error(`${resp.status} ${text || resp.statusText}`); + } + } else { + const client = getOpenAIClient(apiKey, apiUrl); + await client.chat.completions.create({ + model, + messages: [ + { + role: 'user', + content: + 'Hello! This is a test message. Please respond with "API key test successful!"', + }, + ], + max_tokens: 50, + temperature: 0.1, + }); + } logger.info('API key test successful'); return { success: true }; @@ -442,36 +578,341 @@ async function testApiKey( async function makeAIRequest( config: ReturnType, conversation: ConversationMessage[], + interaction?: ChatInputCommandInteraction, + client?: BotClient, + maxIterations = 3, ): Promise { try { - const client = getOpenAIClient(config.finalApiKey!, config.finalApiUrl); + const openAIClient = getOpenAIClient(config.finalApiKey!, config.finalApiUrl); const maxTokens = config.usingDefaultKey ? 1000 : 3000; + const currentConversation = [...conversation]; + let iteration = 0; + let finalResponse: AIResponse | null = null; - const completion = await client.chat.completions.create({ - model: config.finalModel, - messages: conversation as OpenAI.Chat.Completions.ChatCompletionMessageParam[], - max_tokens: maxTokens, - }); + while (iteration < maxIterations) { + iteration++; - const message = completion.choices[0]?.message; - if (!message?.content) { - logger.error('No valid response content from AI API'); - return null; - } + const configuredHost = (() => { + try { + return new URL(config.finalApiUrl).hostname; + } catch (_e) { + // ignore and fallback + } + })(); + + let completion: unknown; + + if (configuredHost === 'generativelanguage.googleapis.com') { + const promptText = currentConversation + .map((m) => { + const role = + m.role === 'system' ? 'System' : m.role === 'assistant' ? 'Assistant' : 'User'; + let text = ''; + if (typeof m.content === 'string') { + text = m.content; + } else if (Array.isArray(m.content)) { + text = m.content + .map((c) => { + if (typeof c === 'string') return c; + const crow = c as Record; + const typeVal = crow['type']; + if (typeVal === 'text') { + const t = crow['text']; + if (typeof t === 'string') return t; + } + const imageObj = crow['image_url']; + if (imageObj && typeof imageObj === 'object') { + const urlVal = (imageObj as Record)['url']; + if (typeof urlVal === 'string') return urlVal; + } + return ''; + }) + .join('\n'); + } + return `${role}: ${text}`; + }) + .join('\n\n'); + + const base = config.finalApiUrl.replace(/\/$/, ''); + const mdl = config.finalModel.startsWith('models/') + ? config.finalModel + : `models/${config.finalModel}`; + const endpoint = `${base}/v1beta/${mdl}:generateContent?key=${encodeURIComponent( + config.finalApiKey || '', + )}`; + + const body: Record = { + contents: [ + { + parts: [ + { + text: promptText, + }, + ], + }, + ], + temperature: 0.2, + maxOutputTokens: Math.min(maxTokens, 3000), + }; + + const resp = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + if (!resp.ok) { + const text = await resp.text(); + throw new Error(`Gemini request failed: ${resp.status} ${text || resp.statusText}`); + } + + const json: unknown = await resp.json(); + + const extractTextFromGemini = (obj: unknown): string | null => { + if (!obj) return null; + try { + const o = obj as Record; + if (Array.isArray(o.candidates) && o.candidates.length) { + const cand = o.candidates[0] as unknown; + if (typeof cand === 'string') return cand; + if (typeof (cand as Record).output === 'string') { + return (cand as Record).output as string; + } + if (Array.isArray((cand as Record).content)) { + return ((cand as Record).content as unknown[]) + .map((p) => { + const pr = p as Record; + if (typeof pr?.text === 'string') return String(pr.text); + if (pr?.type === 'outputText' && typeof pr?.text === 'string') { + return String(pr.text); + } + return ''; + }) + .filter(Boolean) + .join('\n'); + } + if (typeof (cand as Record).output === 'object') { + const outObj = (cand as Record).output as Record; + if (Array.isArray(outObj.content)) { + return (outObj.content as unknown[]) + .map( + (p) => + (p as Record)?.text || + (p as Record)?.textRaw || + '', + ) + .filter(Boolean) + .join('\n'); + } + } + } + + if (Array.isArray(o.outputs) && o.outputs.length) { + const out = o.outputs[0] as unknown; + if (typeof out === 'string') return out; + if (Array.isArray((out as Record).content)) { + return ((out as Record).content as unknown[]) + .map((p) => ((p as Record)?.text as string) || '') + .filter(Boolean) + .join('\n'); + } + } + + const seen = new Set(); + const queue: unknown[] = [obj]; + while (queue.length) { + const cur = queue.shift(); + if (!cur || typeof cur === 'string') { + if (typeof cur === 'string' && cur.trim().length > 0) return cur; + continue; + } + if (seen.has(cur)) continue; + seen.add(cur); + if (Array.isArray(cur)) { + for (const item of cur) queue.push(item); + } else if (typeof cur === 'object') { + const curObj = cur as Record; + for (const k of Object.keys(curObj)) { + const v = curObj[k]; + if (typeof v === 'string' && v.trim().length > 0) return v; + queue.push(v); + } + } + } + } catch (_e) { + // ignore + } + return null; + }; + + const extracted = extractTextFromGemini(json); + if (!extracted) { + throw new Error('Failed to parse Gemini response into text'); + } + + completion = { choices: [{ message: { content: extracted } }] } as unknown; + } else { + completion = (await openAIClient.chat.completions.create({ + model: config.finalModel, + messages: currentConversation as OpenAI.Chat.Completions.ChatCompletionMessageParam[], + max_tokens: maxTokens, + })) as unknown; + } + + const completionTyped = completion as { + choices?: Array<{ message?: { content?: string; reasoning?: string } }>; + }; + const message = completionTyped.choices?.[0]?.message; + if (!message?.content) { + logger.error('No valid response content from AI API'); + return null; + } + + let content = message.content; + let reasoning = (message as OpenAIMessageWithReasoning)?.reasoning; + let detectedCitations: string[] | undefined; + try { + interface CitationSource { + citations?: unknown[]; + metadata?: { + citations?: unknown[]; + [key: string]: unknown; + }; + [key: string]: unknown; + } + + interface CompletionSource { + citations?: unknown[]; + choices?: Array<{ + message?: { + citations?: unknown[]; + [key: string]: unknown; + }; + [key: string]: unknown; + }>; + [key: string]: unknown; + } + + const mAny = message as unknown as CitationSource; + const cAny = completion as unknown as CompletionSource; + const candidates = [ + mAny?.citations, + mAny?.metadata?.citations, + cAny?.citations, + cAny?.choices?.[0]?.message?.citations, + cAny?.choices?.[0]?.citations, + mAny?.references, + mAny?.metadata?.references, + cAny?.references, + ]; + for (const arr of candidates) { + if (Array.isArray(arr) && arr.length) { + const urls = arr.filter((x: unknown) => typeof x === 'string'); + if (urls.length > 0) { + detectedCitations = urls.map(String).filter(Boolean); + break; + } + } + } + } catch (error) { + logger.warn('Error processing citations:', error); + if (error instanceof Error) { + logger.debug('Error processing citations details:', error.stack); + } + } - let content = message.content; - let reasoning = (message as OpenAIMessageWithReasoning)?.reasoning; + let toolCalls: ToolCall[] = []; + if (interaction && client) { + try { + const extraction = extractToolCalls(content); + content = extraction.cleanContent; + toolCalls = extraction.toolCalls; + } catch (error) { + logger.error(`Error extracting tool calls: ${error}`); + toolCalls = []; + } + } - const reasoningMatch = content.match(/```(?:reasoning|thoughts?|thinking)[\s\S]*?```/i); - if (reasoningMatch && !reasoning) { - reasoning = reasoningMatch[0].replace(/```(?:reasoning|thoughts?|thinking)?/gi, '').trim(); - content = content.replace(reasoningMatch[0], '').trim(); + const reasoningMatch = content.match(/```(?:reasoning|thoughts?|thinking)[\s\S]*?```/i); + if (reasoningMatch && !reasoning) { + reasoning = reasoningMatch[0].replace(/```(?:reasoning|thoughts?|thinking)?/gi, '').trim(); + content = content.replace(reasoningMatch[0], '').trim(); + } + + if (toolCalls.length > 0 && interaction && client) { + currentConversation.push({ + role: 'assistant', + content: message.content, + }); + + for (const toolCall of toolCalls) { + try { + const toolResult = await executeToolCall(toolCall, interaction, client); + + let parsedResult; + try { + parsedResult = typeof toolResult === 'string' ? JSON.parse(toolResult) : toolResult; + } catch (_e) { + logger.error(`Error parsing tool result:`, toolResult); + parsedResult = { error: 'Failed to parse tool result' }; + } + + currentConversation.push({ + role: 'user', + content: JSON.stringify({ + type: toolCall.name, + ...parsedResult, + }), + }); + } catch (error) { + logger.error(`Error executing tool call: ${error}`); + currentConversation.push({ + role: 'user', + content: `[Error executing tool ${toolCall.name}]: ${error instanceof Error ? error.message : String(error)}`, + }); + } + } + continue; + } + + finalResponse = { + content, + reasoning, + citations: detectedCitations, + toolResults: + iteration > 1 + ? currentConversation + .filter( + (msg) => + msg.role === 'user' && + typeof msg.content === 'string' && + (msg.content.startsWith('{"') || msg.content.startsWith('[Tool ')), + ) + .map((msg) => { + try { + if (Array.isArray(msg.content)) { + return msg.content + .map((c) => ('text' in c ? c.text : c.image_url?.url)) + .join('\n'); + } + + const content = String(msg.content); + if (content.startsWith('{"') || content.startsWith('[')) { + return content; + } + return content.replace(/^\[Tool [^\]]+\]: /, ''); + } catch (e) { + logger.error('Error processing tool result:', e); + return 'Error processing tool result'; + } + }) + .join('\n') + : undefined, + }; + break; } - return { - content, - reasoning, - }; + return finalResponse; } catch (error) { logger.error(`Error making AI request: ${error}`); return null; @@ -480,36 +921,65 @@ async function makeAIRequest( async function processAIRequest( client: BotClient, - interaction: ChatInputCommandInteraction, + interaction: ChatInputCommandInteraction | ModalSubmitInteraction, + promptOverride?: string, ): Promise { try { if (!interaction.deferred && !interaction.replied) { await interaction.deferReply(); } - const prompt = interaction.options.getString('prompt')!; - commandLogger.logFromInteraction( - interaction, - `AI command executed - prompt content hidden for privacy`, - ); - const invokerId = getInvokerId(interaction); - const { apiKey, model, apiUrl } = await getUserCredentials(invokerId); + const prompt = + promptOverride ?? + ((interaction as ChatInputCommandInteraction).options?.getString?.('prompt') as + | string + | null + | undefined) ?? + (pendingRequests.get(invokerId)?.prompt as string); + + if (!prompt) { + await interaction.editReply('❌ Missing prompt. Please try again.'); + return; + } + if ('commandType' in interaction) { + commandLogger.logFromInteraction( + interaction as ChatInputCommandInteraction, + `AI command executed - prompt content hidden for privacy`, + ); + } else { + commandLogger.logAction({ + isGuild: interaction.inGuild(), + isDM: !interaction.inGuild(), + additionalInfo: `AI command executed - prompt content hidden for privacy`, + }); + } + const { apiKey, model, apiUrl } = await getUserCredentials(interaction.user.id); const config = getApiConfiguration(apiKey ?? null, model ?? null, apiUrl ?? null); + const exemptUserId = process.env.AI_EXEMPT_USER_ID; - if (config.usingDefaultKey) { - const exemptUserId = process.env.AI_EXEMPT_USER_ID; - if (invokerId !== exemptUserId) { - const allowed = await incrementAndCheckDailyLimit(invokerId, 10); - if (!allowed) { + if (invokerId !== exemptUserId && config.usingDefaultKey) { + const allowed = await incrementAndCheckDailyLimit(interaction.user.id, 10); + if (!allowed) { + await interaction.editReply( + '❌ ' + + (await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)), + ); + return; + } + + if (interaction.inGuild()) { + const serverAllowed = await incrementAndCheckServerDailyLimit(interaction.guildId!, 50); + if (!serverAllowed) { await interaction.editReply( - '❌ ' + - (await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)), + '❌ This server has reached its daily AI usage limit. Please try again tomorrow.', ); return; } } - } else if (!config.finalApiKey) { + } + + if (!config.finalApiKey && config.usingDefaultKey) { await interaction.editReply( '❌ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale)), ); @@ -517,19 +987,23 @@ async function processAIRequest( } const existingConversation = userConversations.get(invokerId) || []; - const conversationArray = Array.isArray(existingConversation) ? existingConversation : []; + const _conversationArray = Array.isArray(existingConversation) ? existingConversation : []; + const chatInputInteraction = + 'commandType' in interaction ? (interaction as ChatInputCommandInteraction) : undefined; + const systemPrompt = buildSystemPrompt( - !!config.usingDefaultKey, + config.usingDefaultKey, client, config.finalModel, - interaction.user.tag, - interaction, + interaction.user.username, + chatInputInteraction, interaction.inGuild(), interaction.inGuild() ? interaction.guild?.name : undefined, ); - const conversation = buildConversation(conversationArray, prompt, systemPrompt); - const aiResponse = await makeAIRequest(config, conversation); + const conversation = buildConversation(existingConversation, prompt, systemPrompt); + + const aiResponse = await makeAIRequest(config, conversation, chatInputInteraction, client, 3); if (!aiResponse) return; const { getUnallowedWordCategory } = await import('@/utils/validation'); @@ -554,90 +1028,266 @@ async function processAIRequest( await sendAIResponse(interaction, aiResponse, client); } catch (error) { - await errorHandler({ - interaction, - client, - error: error as Error, - userId: getInvokerId(interaction), - username: interaction.user.tag, - }); + const err = error as Error; + if ( + 'commandType' in interaction || + 'componentType' in (interaction as unknown as Record) + ) { + await errorHandler({ + interaction: interaction as unknown as ChatInputCommandInteraction, + client, + error: err, + userId: getInvokerId(interaction), + username: interaction.user.tag, + }); + } else { + const msg = await client.getLocaleText('failedrequest', interaction.locale); + try { + if (interaction.deferred || interaction.replied) { + await interaction.editReply(msg); + } else { + await interaction.reply({ content: msg, flags: MessageFlags.Ephemeral }); + } + } catch (replyError) { + logger.error(`Failed to send error message for AI command: ${replyError}`); + } + logger.error(`Error in AI command for user ${interaction.user.tag}: ${err.message}`); + } } finally { pendingRequests.delete(getInvokerId(interaction)); } } async function sendAIResponse( - interaction: ChatInputCommandInteraction, + interaction: ChatInputCommandInteraction | ModalSubmitInteraction, aiResponse: AIResponse, - client: BotClient, + _client: BotClient, ): Promise { - let fullResponse = ''; + try { + let fullResponse = ''; + + if (aiResponse.reasoning) { + const cleanedReasoning = aiResponse.reasoning + .split('\n') + .map((line: string) => line.trim()) + .filter((line: string) => line) + .join('\n'); + + const formattedReasoning = cleanedReasoning + .split('\n') + .map((line: string) => `> ${line}`) + .join('\n'); + + fullResponse = `${formattedReasoning}\n\n${aiResponse.content}`; + aiResponse.content = ''; + } - if (aiResponse.reasoning) { - const cleanedReasoning = aiResponse.reasoning - .split('\n') - .map((line) => line.trim()) - .filter((line) => line) - .join('\n'); + fullResponse += aiResponse.content; - const formattedReasoning = cleanedReasoning - .split('\n') - .map((line) => `> ${line}`) - .join('\n'); + try { + if (aiResponse.citations && aiResponse.citations.length && fullResponse) { + fullResponse = fullResponse.replace(/\[(\d+)\](?!\()/g, (match: string, numStr: string) => { + const idx = parseInt(numStr, 10) - 1; + const url = aiResponse.citations![idx]; + if (typeof url === 'string' && url.trim()) { + return `[${numStr}](${url.trim()})`; + } + return match; + }); + } + } catch (e) { + logger.warn('Failed to inline citation sources', e); + } - fullResponse = `${formattedReasoning}\n\n${aiResponse.content}`; - aiResponse.content = ''; - } + if (aiResponse.toolResults) { + try { + const toolResults = Array.isArray(aiResponse.toolResults) + ? aiResponse.toolResults + : [aiResponse.toolResults]; + + for (const result of toolResults) { + try { + let toolResult; + if (typeof result === 'string') { + try { + toolResult = JSON.parse(result); + } catch (parseError) { + logger.error(`[AI] Error parsing tool result JSON:`, { + error: parseError, + result: result.substring(0, 200) + '...', + }); + continue; + } + } else { + toolResult = result; + } - fullResponse += aiResponse.content; + if ((toolResult.type === 'cat' || toolResult.type === 'dog') && toolResult.url) { + let cleanContent = aiResponse.content || ''; + if (toolResult.url) { + cleanContent = cleanContent.replace(/!\[[^\]]*\]\([^)]*\)/g, '').trim(); + cleanContent = cleanContent.replace(toolResult.url, '').trim(); + } + + await interaction.editReply({ + content: cleanContent || undefined, + files: [ + { + attachment: toolResult.url, + name: `${toolResult.type}.jpg`, + }, + ], + }); + return; + } + } catch (parseError) { + logger.error('Error parsing individual tool result:', parseError); + } + } + } catch (error) { + logger.error('Error processing tool results:', error); + } + } - const { getUnallowedWordCategory } = await import('@/utils/validation'); - const category = getUnallowedWordCategory(fullResponse); - if (category) { - logger.warn(`AI response contained unallowed words in category: ${category}`); - await interaction.editReply( - 'Sorry, I cannot provide that response as it contains prohibited content. Please try a different prompt.', - ); - return; - } + const { getUnallowedWordCategory } = await import('@/utils/validation'); + const category = getUnallowedWordCategory(fullResponse); + if (category) { + logger.warn(`AI response contained unallowed words in category: ${category}`); + await interaction.editReply( + 'Sorry, I cannot provide that response as it contains prohibited content. Please try a different prompt.', + ); + return; + } - const urlProcessedResponse = processUrls(fullResponse); - const chunks = splitResponseIntoChunks(urlProcessedResponse); + const urlProcessedResponse = processUrls(fullResponse); + const chunks = splitResponseIntoChunks(urlProcessedResponse); - try { await interaction.editReply(chunks[0]); for (let i = 1; i < chunks.length; i++) { await interaction.followUp({ content: chunks[i], - flags: MessageFlags.Ephemeral, + flags: MessageFlags.SuppressNotifications, }); } - } catch { + } catch (error) { + logger.error('Error in sendAIResponse:', error); try { - const fallbackMessage = `${chunks[0]}\n\n*❌ ${await client.getLocaleText('commands.ai.errors.toolong', interaction.locale)}*`; - await interaction.editReply(fallbackMessage); - } catch { - logger.error('Failed to send AI response fallback message'); + await interaction.editReply('An error occurred while processing your request.'); + } catch (editError) { + logger.error('Failed to send error message:', editError); + } + return; + } + + if (aiResponse.toolResults) { + try { + const toolResult = JSON.parse(aiResponse.toolResults); + + if (toolResult.alreadyResponded && !aiResponse.content) { + return; + } + + if (toolResult.type === 'command') { + if (toolResult.image) { + const embed = new EmbedBuilder().setImage(toolResult.image).setColor(0x8a2be2); + + if (toolResult.title) { + embed.setTitle(toolResult.title); + } + if (toolResult.source) { + embed.setFooter({ text: `Source: ${toolResult.source}` }); + } + + try { + await interaction.followUp({ + embeds: [embed], + flags: MessageFlags.SuppressNotifications, + }); + return; + } catch (error) { + logger.error('Failed to send embed with source:', error); + return; + } + } + + if (toolResult.success && toolResult.data) { + const components = toolResult.data.components || []; + let imageUrl: string | null = null; + let caption = ''; + + for (const component of components) { + if (component.type === 12 && component.items?.[0]?.media?.url) { + imageUrl = component.items[0].media.url; + break; + } + } + + for (const component of components) { + if (component.type === 10 && component.content) { + caption = component.content; + break; + } + } + + if (imageUrl) { + await interaction.followUp({ + content: caption || undefined, + files: [imageUrl], + flags: MessageFlags.SuppressNotifications, + }); + return; + } + } + + if (aiResponse.toolResults) { + await interaction.followUp({ + content: aiResponse.toolResults, + flags: MessageFlags.SuppressNotifications, + }); + } + } + } catch (error) { + console.error('Error processing tool results:', error); + try { + await interaction.followUp({ + content: 'An error occurred while processing the tool results.', + flags: MessageFlags.SuppressNotifications, + }); + } catch (followUpError) { + console.error('Failed to send error message:', followUpError); + } } } } +export type { ConversationMessage, AIResponse }; + export { makeAIRequest, getApiConfiguration, buildSystemPrompt, buildConversation, - sendAIResponse, getUserCredentials, incrementAndCheckDailyLimit, incrementAndCheckServerDailyLimit, splitResponseIntoChunks, }; -export type { ConversationMessage, AIResponse }; +interface AICommand { + data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; + execute: ( + client: BotClient, + interaction: ChatInputCommandInteraction, + ) => Promise>; + handleModal: ( + client: BotClient, + interaction: ModalSubmitInteraction, + ) => Promise>; + handleSelect: (client: BotClient, interaction: StringSelectMenuInteraction) => Promise; +} -export default { +const aiCommand: AICommand = { data: new SlashCommandBuilder() .setName('ai') .setNameLocalizations({ @@ -660,11 +1310,6 @@ export default { .addStringOption((option) => option .setName('prompt') - .setNameLocalizations({ - 'es-ES': 'mensaje', - 'es-419': 'mensaje', - 'en-US': 'prompt', - }) .setDescription('Your message to the AI') .setDescriptionLocalizations({ 'es-ES': 'Tu mensaje para la IA', @@ -685,14 +1330,15 @@ export default { if (pendingRequests.has(userId)) { const pending = pendingRequests.get(userId); - if (pending && Date.now() - pending.timestamp > 30000) { - pendingRequests.delete(userId); - } else { + const isProcessing = pending?.status === 'processing'; + const isExpired = pending ? Date.now() - pending.createdAt > 30000 : true; + if (isProcessing && !isExpired) { return interaction.reply({ content: await client.getLocaleText('commands.ai.request.inprogress', interaction.locale), flags: MessageFlags.Ephemeral, }); } + pendingRequests.delete(userId); } try { @@ -700,7 +1346,12 @@ export default { const prompt = interaction.options.getString('prompt')!; const reset = interaction.options.getBoolean('reset'); - pendingRequests.set(userId, { interaction, prompt, timestamp: Date.now() }); + pendingRequests.set(userId, { + interaction, + prompt, + createdAt: Date.now(), + status: 'awaiting', + }); if (reset) { userConversations.delete(userId); @@ -713,7 +1364,7 @@ export default { } if (useCustomApi === false) { - await setUserApiKey(userId, null, null, null); + await setUserApiKey(interaction.user.id, null, null, null); userConversations.delete(userId); await interaction.reply({ content: await client.getLocaleText('commands.ai.defaultapi', interaction.locale), @@ -723,94 +1374,120 @@ export default { return; } - const { apiKey } = await getUserCredentials(userId); + const { apiKey } = await getUserCredentials(interaction.user.id); if (useCustomApi && !apiKey) { - const modal = new ModalBuilder() - .setCustomId('apiCredentials') - .setTitle(await client.getLocaleText('commands.ai.modal.title', interaction.locale)); - - const apiKeyInput = new TextInputBuilder() - .setCustomId('apiKey') - .setLabel(await client.getLocaleText('commands.ai.modal.apikey', interaction.locale)) - .setStyle(TextInputStyle.Short) - .setPlaceholder( - await client.getLocaleText('commands.ai.modal.apikeyplaceholder', interaction.locale), - ) - .setRequired(true); - - const apiUrlInput = new TextInputBuilder() - .setCustomId('apiUrl') - .setLabel(await client.getLocaleText('commands.ai.modal.apiurl', interaction.locale)) - .setStyle(TextInputStyle.Short) - .setPlaceholder( - await client.getLocaleText('commands.ai.modal.apiurlplaceholder', interaction.locale), - ) - .setRequired(true); - - const modelInput = new TextInputBuilder() - .setCustomId('model') - .setLabel(await client.getLocaleText('commands.ai.modal.model', interaction.locale)) - .setStyle(TextInputStyle.Short) - .setPlaceholder( - await client.getLocaleText('commands.ai.modal.modelplaceholder', interaction.locale), - ) - .setRequired(true); - - modal.addComponents( - new ActionRowBuilder().addComponents(apiKeyInput), - new ActionRowBuilder().addComponents(apiUrlInput), - new ActionRowBuilder().addComponents(modelInput), - ); - - await interaction.showModal(modal); - } else { - await interaction.deferReply(); - await processAIRequest(client, interaction); + const title = await client.getLocaleText('commands.ai.modal.title', interaction.locale); + const v2Modal = buildProviderModal('apiCredentials', title); + await showV2Modal(interaction, v2Modal); + return; } - } catch { - pendingRequests.delete(userId); - const errorMessage = await client.getLocaleText('failedrequest', interaction.locale); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ content: errorMessage, flags: MessageFlags.Ephemeral }); - } else { - await interaction.editReply({ content: errorMessage }); + await processAIRequest(client, interaction); + } catch (error) { + console.error('Error in AI command:', error); + const errorMessage = `❌ An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`; + + try { + if (interaction.replied || interaction.deferred) { + await interaction.editReply({ content: errorMessage }); + } else { + await interaction.reply({ + content: errorMessage, + flags: MessageFlags.Ephemeral, + }); + } + } catch (replyError) { + console.error('Failed to send error message:', replyError); } + + const userId = getInvokerId(interaction); + pendingRequests.delete(userId); } }, async handleModal(client: BotClient, interaction: ModalSubmitInteraction) { try { - if (interaction.customId === 'apiCredentials') { + if (interaction.customId.startsWith('apiConfig')) { + console.log('Handling API configuration modal submission'); + try { + await interaction.reply({ + content: 'βœ… Provider noted. Please submit credentials next.', + flags: MessageFlags.Ephemeral, + }); + } catch (replyError) { + console.error('Failed to send apiConfig acknowledgement:', replyError); + } + } else if (interaction.customId.startsWith('apiCredentials')) { + console.log('Handling API credentials modal submission'); await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const userId = getInvokerId(interaction); const pendingRequest = pendingRequests.get(userId); if (!pendingRequest) { - return interaction.editReply( - await client.getLocaleText('commands.ai.nopendingrequest', interaction.locale), - ); + console.error('No pending request found for user:', userId); + return interaction + .editReply({ + content: '❌ No pending AI request found. Please try your request again.', + }) + .catch(console.error); + } + + let apiKey = ''; + let model = ''; + let providerValue: string | undefined; + + const parts = interaction.customId.split(':'); + if (parts.length > 1) { + providerValue = parts[1]; } - const { interaction: originalInteraction } = pendingRequest; - const apiKey = interaction.fields.getTextInputValue('apiKey').trim(); - const apiUrl = interaction.fields.getTextInputValue('apiUrl').trim(); - const model = interaction.fields.getTextInputValue('model').trim(); + try { + try { + apiKey = interaction.fields.getTextInputValue('apiKey')?.trim() || ''; + model = interaction.fields.getTextInputValue('model')?.trim() || ''; + } catch (_error) { + console.log('Could not get values from text inputs, trying V2 components'); + } + } catch (error) { + console.error('Error processing provider value:', error); + } + if (!providerValue) { + const submitted = parseV2ModalSubmission( + interaction as unknown as Record, + ); + providerValue = Array.isArray(submitted.provider) + ? submitted.provider[0] + : (submitted.provider as string | undefined); + } + if (!providerValue) { + const m = model.toLowerCase(); + if (m.startsWith('models/gemini') || m.startsWith('gemini')) providerValue = 'gemini'; + else if (m.startsWith('pplx') || m.includes('perplexity')) providerValue = 'perplexity'; + else if (m.startsWith('deepseek')) providerValue = 'deepseek'; + else if (m.startsWith('moonshot') || m.includes('kimi')) providerValue = 'moonshot'; + else if (m.includes('/')) providerValue = 'openrouter'; + else providerValue = 'openai'; + } + if (!apiKey || !model) { + await interaction.editReply( + 'Missing API key or model from modal submission. Please try again.', + ); + return; + } + const apiUrl = providerValue + ? PROVIDER_TO_URL[providerValue] + : 'https://openrouter.ai/api/v1'; let parsedUrl; try { parsedUrl = new URL(apiUrl); } catch { - await interaction.editReply( - 'API URL is invalid. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).', - ); + await interaction.editReply('API provider URL is invalid.'); return; } if (!ALLOWED_API_HOSTS.includes(parsedUrl.hostname)) { - await interaction.editReply( - 'API URL not allowed. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).', - ); + await interaction.editReply('Selected provider is not allowed.'); return; } @@ -830,29 +1507,77 @@ export default { return; } - await setUserApiKey(userId, apiKey, model, apiUrl); + await setUserApiKey(interaction.user.id, apiKey, model, apiUrl); await interaction.editReply( await client.getLocaleText('commands.ai.testsuccess', interaction.locale), ); - if (!originalInteraction.deferred && !originalInteraction.replied) { - await originalInteraction.deferReply(); - } - await processAIRequest(client, originalInteraction); + await processAIRequest(client, interaction, pendingRequest.prompt); + } + } catch (error) { + try { + await interaction.editReply({ + content: await client.getLocaleText('failedrequest', interaction.locale), + }); + } catch (editError) { + console.error('Failed to send error reply in handleModal:', editError || error); } - } catch { - await interaction.editReply({ - content: await client.getLocaleText('failedrequest', interaction.locale), - }); } finally { pendingRequests.delete(getInvokerId(interaction)); + try { + const clientInstance = BotClient.getInstance(); + const typedClient = clientInstance as ClientWithModalState; + if (typedClient.lastModalRawByUser) { + typedClient.lastModalRawByUser.delete(interaction.user.id); + } + } catch (error) { + logger.warn('Error cleaning up modal state:', error); + } + } + }, + handleSelect: async (client: BotClient, interaction: StringSelectMenuInteraction) => { + try { + if (interaction.customId !== 'ai_provider_select') return; + + const provider = interaction.values[0]; + const modalTitle = await client.getLocaleText('commands.ai.modal.title', interaction.locale); + + const modal = new ModalBuilder() + .setCustomId(`apiCredentials:${provider}`) + .setTitle(modalTitle); + + const apiKeyInput = new TextInputBuilder() + .setCustomId('apiKey') + .setLabel('API Key') + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setPlaceholder('Paste your provider API key'); + + const modelInput = new TextInputBuilder() + .setCustomId('model') + .setLabel('Model') + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setPlaceholder('e.g., gpt-4o-mini, gemini-1.5-pro'); + + modal.addComponents( + new ActionRowBuilder().addComponents(apiKeyInput), + new ActionRowBuilder().addComponents(modelInput), + ); + + await interaction.showModal(modal); + } catch (error) { + console.error('Error in handleSelect:', error); + try { + await interaction.reply({ + content: '❌ Failed to show API configuration modal. Please try again.', + flags: MessageFlags.Ephemeral, + }); + } catch (replyError) { + console.error('Failed to send error message:', replyError); + } } }, -} as unknown as SlashCommandProps; +}; -function getInvokerId(interaction: ChatInputCommandInteraction | ModalSubmitInteraction) { - if (interaction.inGuild()) { - return `${interaction.guildId}-${interaction.user.id}`; - } - return interaction.user.id; -} +export default aiCommand as unknown as SlashCommandProps; diff --git a/src/commands/utilities/wiki.ts b/src/commands/utilities/wiki.ts index d484cee..8c2c62f 100644 --- a/src/commands/utilities/wiki.ts +++ b/src/commands/utilities/wiki.ts @@ -24,7 +24,7 @@ const errorHandler = createErrorHandler('wiki'); const MAX_EXTRACT_LENGTH = 2000; -async function searchWikipedia(query: string, locale = 'en') { +export async function searchWikipedia(query: string, locale = 'en') { const wikiLang = locale.startsWith('es') ? 'es' : 'en'; const searchUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&srlimit=1`; @@ -46,7 +46,7 @@ async function searchWikipedia(query: string, locale = 'en') { }; } -async function getArticleSummary(pageId: number, wikiLang = 'en') { +export async function getArticleSummary(pageId: number, wikiLang = 'en') { const summaryUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&prop=extracts|pageimages&exintro&explaintext&format=json&pithumbsize=300&pageids=${pageId}`; const response = await fetch(summaryUrl); diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 68016fc..9b97302 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -89,7 +89,7 @@ export default class InteractionCreateEvent { if (remind && remind.handleModal) { await remind.handleModal(this.client, i); } - } else if (i.customId === 'apiCredentials') { + } else if (i.customId.startsWith('apiCredentials')) { const ai = this.client.commands.get('ai'); if (ai && 'handleModal' in ai) { await (ai as unknown as RemindCommandProps).handleModal(this.client, i); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 32044da..4d27b95 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -12,6 +12,9 @@ import { splitResponseIntoChunks, processUrls, } from '@/commands/utilities/ai'; +import { extractToolCalls as extractSlashToolCalls } from '@/utils/commandExecutor'; +import fetch from '@/utils/dynamicFetch'; +import { executeMessageToolCall, type MessageToolCall } from '@/utils/messageToolExecutor'; import type { ConversationMessage, AIResponse } from '@/commands/utilities/ai'; type ApiConfiguration = ReturnType; @@ -42,6 +45,15 @@ const userConversations = createMemoryManager({ cleanupInterval: 10 * 60 * 1000, }); +function extractMessageToolCalls(content: string): { + cleanContent: string; + toolCalls: MessageToolCall[]; +} { + const { cleanContent, toolCalls } = extractSlashToolCalls(content); + + return { cleanContent, toolCalls }; +} + function getServerConversationKey(guildId: string): string { return `server:${guildId}`; } @@ -114,7 +126,7 @@ export default class MessageCreateEvent { model: userCustomModel, apiKey: userApiKey, apiUrl: userApiUrl, - } = await getUserCredentials(`user:${message.author.id}`); + } = await getUserCredentials(message.author.id); selectedModel = hasImages ? 'google/gemma-3-4b-it' @@ -126,6 +138,14 @@ export default class MessageCreateEvent { selectedModel = hasImages ? 'google/gemma-3-4b-it' : 'google/gemini-2.5-flash-lite'; config = getApiConfiguration(null, selectedModel, null); + if (config.usingDefaultKey && !config.finalApiKey) { + await message.reply({ + content: + '❌ AI is not configured. Please set OPENROUTER_API_KEY on the bot, or use `/ai` with your own API key.', + allowedMentions: { parse: ['users'] as const }, + }); + return; + } } logger.info( @@ -298,12 +318,14 @@ export default class MessageCreateEvent { return; } - let aiResponse = await makeAIRequest(config, updatedConversation); + const conversationWithTools = [...updatedConversation]; + const executedResults: Array<{ type: string; payload: Record }> = []; + let aiResponse = await makeAIRequest(config, conversationWithTools); if (!aiResponse && hasImages) { logger.warn(`First attempt failed for ${selectedModel}, retrying once...`); await new Promise((resolve) => setTimeout(resolve, 1000)); - aiResponse = await makeAIRequest(config, updatedConversation); + aiResponse = await makeAIRequest(config, conversationWithTools); } if (!aiResponse && hasImages) { @@ -365,6 +387,153 @@ export default class MessageCreateEvent { } } + const maxIterations = 3; + let iteration = 0; + while (aiResponse && iteration < maxIterations) { + iteration++; + const extraction = extractMessageToolCalls(aiResponse.content || ''); + const toolCalls: MessageToolCall[] = extraction.toolCalls; + + if (toolCalls.length === 0) { + aiResponse.content = extraction.cleanContent; + break; + } + + conversationWithTools.push({ role: 'assistant', content: aiResponse.content }); + + for (const tc of toolCalls) { + const name = tc.name?.toLowerCase(); + try { + if (name === 'cat' || name === 'dog') { + const isCat = name === 'cat'; + const url = isCat + ? 'https://api.pur.cat/random-cat' + : 'https://api.erm.dog/random-dog'; + const res = await fetch(url); + let imageUrl = ''; + try { + const data = await res.json(); + imageUrl = data?.url || ''; + if (!imageUrl && !isCat) { + const res2 = await fetch(url); + imageUrl = await res2.text(); + } + } catch { + const res2 = await fetch(url); + imageUrl = await res2.text(); + } + const payload = imageUrl + ? { type: name, url: imageUrl } + : { type: name, error: 'no_image' }; + executedResults.push({ type: name, payload }); + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); + } else if (name === 'weather') { + const raw = (tc.args?.location as string) || (tc.args?.query as string) || ''; + const location = typeof raw === 'string' ? raw.trim() : ''; + const apiKey = process.env.OPENWEATHER_API_KEY; + let payload: Record = { type: 'weather', location }; + if (location && apiKey) { + try { + const resp = await fetch( + `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&appid=${apiKey}&units=imperial`, + ); + if (resp.ok) { + const data = await resp.json(); + payload = { + type: 'weather', + location: data.name || location, + temperature: `${Math.round(data.main?.temp)}Β°F`, + feels_like: `${Math.round(data.main?.feels_like)}Β°F`, + conditions: data.weather?.[0]?.description || 'Unknown', + humidity: `${data.main?.humidity}%`, + wind_speed: `${Math.round(data.wind?.speed)} mph`, + pressure: `${data.main?.pressure} hPa`, + }; + } else { + logger.error('[MessageCreate] Weather fetch failed', { + status: resp.status, + statusText: resp.statusText, + }); + payload = { + type: 'weather', + location, + error: `${resp.status} ${resp.statusText}`, + }; + } + } catch (e) { + logger.error('[MessageCreate] Weather fetch error', { + error: (e as Error)?.message, + }); + payload = { + type: 'weather', + location, + error: (e as Error)?.message || 'fetch_failed', + }; + } + } else { + logger.warn('[MessageCreate] Weather missing params or API key', { + locationPresent: !!location, + apiKeyPresent: !!apiKey, + }); + payload = { type: 'weather', location, error: 'missing_params' }; + } + executedResults.push({ type: 'weather', payload }); + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); + } else if (name === 'wiki') { + const query = (tc.args?.search as string) || (tc.args?.query as string) || ''; + const q = typeof query === 'string' ? query.trim() : ''; + let payload: Record = { type: 'wiki' }; + if (q) { + try { + const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(q)}`; + const res = await fetch(url); + if (res.ok) { + const data = await res.json(); + payload = { + type: 'wiki', + title: data.title || q, + extract: data.extract || '', + url: + data.content_urls?.desktop?.page || + `https://en.wikipedia.org/wiki/${encodeURIComponent(q)}`, + }; + } else { + payload = { type: 'wiki', error: `${res.status} ${res.statusText}` }; + } + } catch (e) { + payload = { type: 'wiki', error: (e as Error)?.message || 'fetch_failed' }; + } + } + executedResults.push({ type: 'wiki', payload }); + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); + } else if (name === 'reaction') { + const emoji = (tc.args?.emoji as string) || ''; + const payload = { type: 'reaction', emoji, deferred: true }; + executedResults.push({ type: 'reaction', payload }); + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); + + try { + await executeMessageToolCall(tc, message, this.client, { + originalMessage: message, + botMessage: undefined, + }); + } catch (error) { + logger.error('[MessageCreate] Iterative loop - reaction execution failed:', error); + } + } + } catch (e) { + conversationWithTools.push({ role: 'user', content: `[Tool ${tc.name} error]` }); + logger.error('[MessageCreate] Tool execution threw exception', { + name: tc.name, + error: (e as Error)?.message, + }); + } + } + + aiResponse = await makeAIRequest(config, conversationWithTools); + if (!aiResponse) break; + } + if (!aiResponse) { await message.reply({ content: 'Sorry, I encountered an error processing your message. Please try again later.', @@ -373,9 +542,62 @@ export default class MessageCreateEvent { return; } + if (!aiResponse.content || !aiResponse.content.trim()) { + const last = executedResults[executedResults.length - 1]; + if (last) { + if ( + (last.type === 'cat' || last.type === 'dog') && + typeof last.payload.url === 'string' + ) { + aiResponse.content = `Here you go ${last.type === 'cat' ? '🐱' : '🐢'}: ${last.payload.url}`; + } else if (last.type === 'weather') { + const p = last.payload as Record; + if (p.location && p.temperature) { + aiResponse.content = `Weather for ${p.location}: ${p.temperature} (feels ${p.feels_like}), ${p.conditions}. Humidity ${p.humidity}, Wind ${p.wind_speed}, Pressure ${p.pressure}.`; + } else if (p.description) { + aiResponse.content = `Weather in ${p.location || 'the requested area'}: ${p.description}`; + } + } else if (last.type === 'wiki') { + const p = last.payload as Record; + if (p.title || p.extract || p.url) { + aiResponse.content = + `${p.title || ''}\n${p.extract || ''}\n${p.url ? `<${p.url}>` : ''}`.trim(); + } + } + } + + if (!aiResponse.content || !aiResponse.content.trim()) { + aiResponse.content = '\u200b'; + } + } + aiResponse.content = processUrls(aiResponse.content); aiResponse.content = aiResponse.content.replace(/@(everyone|here)/gi, '@\u200b$1'); + const originalContent = aiResponse.content || ''; + const extraction = extractMessageToolCalls(originalContent); + aiResponse.content = extraction.cleanContent; + const toolCalls: MessageToolCall[] = extraction.toolCalls; + const hasReactionTool = toolCalls.some((tc) => tc?.name?.toLowerCase() === 'reaction'); + const originalCleaned = (extraction.cleanContent || '').trim(); + + if (!originalCleaned && hasReactionTool) { + for (const tc of toolCalls) { + if (!tc || !tc.name) continue; + const name = tc.name.toLowerCase(); + if (name !== 'reaction') continue; + try { + await executeMessageToolCall(tc, message, this.client, { + originalMessage: message, + botMessage: undefined, + }); + } catch (err) { + logger.error('Error executing reaction tool on original message:', err); + } + } + return; + } + const { getUnallowedWordCategory } = await import('@/utils/validation'); const category = getUnallowedWordCategory(aiResponse.content); if (category) { @@ -388,7 +610,179 @@ export default class MessageCreateEvent { return; } - await this.sendResponse(message, aiResponse); + const cleaned = (aiResponse.content || '').trim(); + const onlyReactions = !cleaned && hasReactionTool; + if (onlyReactions) { + const toolCallRegex = /{([^{}\s:]+):({[^{}]*}|[^{}]*)?}/g; + const fallback = originalContent.replace(toolCallRegex, '').trim(); + if (fallback) { + aiResponse.content = fallback; + } else { + const textParts: string[] = []; + for (const tc of toolCalls) { + if (!tc || !tc.args) continue; + const a = tc.args as Record; + const candidates = ['text', 'query', 'content', 'body', 'message']; + for (const k of candidates) { + const v = a[k] as unknown; + if (typeof v === 'string' && v.trim()) { + textParts.push(v.trim()); + break; + } + } + } + if (textParts.length) { + aiResponse.content = textParts.join(' '); + } else { + const reactionLabels: string[] = []; + for (const tc of toolCalls) { + if (!tc || !tc.name) continue; + if (tc.name.toLowerCase() !== 'reaction') continue; + const a = tc.args as Record; + const emojiCandidate = + (a.emoji as string) || (a.query as string) || (a['emojiRaw'] as string) || ''; + if (typeof emojiCandidate === 'string' && emojiCandidate.trim()) { + reactionLabels.push(emojiCandidate.trim()); + } + } + if (reactionLabels.length) { + aiResponse.content = `Reacted with ${reactionLabels.join(', ')}`; + } else { + aiResponse.content = 'Reacted.'; + } + } + } + } + + const sent = await this.sendResponse(message, aiResponse); + const sentMessage: Message | undefined = sent as Message | undefined; + + if (extraction.toolCalls.length > 0) { + const target = sentMessage || message; + const executed: Array<{ name: string; success: boolean }> = []; + for (const tc of extraction.toolCalls) { + if (!tc || !tc.name) continue; + const name = tc.name.toLowerCase(); + if (name === 'reaction') { + try { + const result = await executeMessageToolCall(tc, target, this.client, { + originalMessage: message, + botMessage: sentMessage, + }); + executed.push({ name, success: !!result?.success }); + } catch (err) { + logger.error('Error executing message tool:', { name, err }); + executed.push({ name, success: false }); + } + } else if (name === 'cat' || name === 'dog') { + try { + const isCat = name === 'cat'; + const url = isCat + ? 'https://api.pur.cat/random-cat' + : 'https://api.erm.dog/random-dog'; + const res = await fetch(url); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + let imageUrl = ''; + try { + const data = await res.json(); + imageUrl = data?.url || ''; + if (!imageUrl && !isCat) { + const res2 = await fetch(url); + imageUrl = await res2.text(); + } + } catch { + const res2 = await fetch(url); + imageUrl = await res2.text(); + } + if (imageUrl && imageUrl.startsWith('http')) { + await target.reply({ content: '', files: [imageUrl] }); + executed.push({ name, success: true }); + } else { + executed.push({ name, success: false }); + } + } catch (err) { + logger.error('Error executing image tool:', { name, err }); + executed.push({ name, success: false }); + } + } else if (name === 'weather') { + try { + const raw = (tc.args?.location as string) || (tc.args?.query as string) || ''; + const location = typeof raw === 'string' ? raw.trim() : ''; + if (!location) { + executed.push({ name, success: false }); + } else { + const apiKey = process.env.OPENWEATHER_API_KEY; + if (!apiKey) { + executed.push({ name, success: false }); + } else { + const resp = await fetch( + `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent( + location, + )}&appid=${apiKey}&units=imperial`, + ); + if (!resp.ok) { + executed.push({ name, success: false }); + } else { + const data = await resp.json(); + const temp = Math.round(data.main?.temp); + const feels = Math.round(data.main?.feels_like); + const cond = data.weather?.[0]?.description || 'Unknown'; + const hum = data.main?.humidity; + const wind = Math.round(data.wind?.speed); + const pres = data.main?.pressure; + await target.reply( + `Weather for ${data.name || location}: ${temp}Β°F (feels ${feels}Β°F), ${cond}. Humidity ${hum}%, Wind ${wind} mph, Pressure ${pres} hPa.`, + ); + executed.push({ name, success: true }); + } + } + } + } catch (err) { + logger.error('Error executing weather tool:', { err }); + executed.push({ name, success: false }); + } + } else if (name === 'wiki') { + try { + const query = (tc.args?.search as string) || (tc.args?.query as string) || ''; + const q = typeof query === 'string' ? query.trim() : ''; + if (!q) { + executed.push({ name, success: false }); + } else { + const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(q)}`; + const res = await fetch(url); + if (!res.ok) { + executed.push({ name, success: false }); + } else { + const data = await res.json(); + const title = data.title || q; + const extract = data.extract || 'No summary available.'; + const pageUrl = + data.content_urls?.desktop?.page || + `https://en.wikipedia.org/wiki/${encodeURIComponent(q)}`; + await target.reply(`${title}\n${extract}\n<${pageUrl}>`); + executed.push({ name, success: true }); + } + } + } catch (err) { + logger.error('Error executing wiki tool:', { err }); + executed.push({ name, success: false }); + } + } + } + + if (onlyReactions) { + const anyFailed = executed.some((e) => e.name === 'reaction' && !e.success); + if (anyFailed && sentMessage) { + try { + await sentMessage.edit( + 'I tried to react, but I do not have permission to add reactions here or the emoji was invalid.', + ); + } catch (e) { + logger.error('Failed to edit placeholder message after reaction failure:', e); + } + } + } + } const userMessage: ConversationMessage = { role: 'user', @@ -413,10 +807,12 @@ export default class MessageCreateEvent { logger.info(`${isDM ? 'DM' : 'Server'} response sent successfully`); } catch (error) { - logger.error( - `Error processing ${isDM ? 'DM' : 'server message'}:`, - error instanceof Error ? error.message : String(error), - ); + const err = error as Error; + logger.error(`Error processing ${isDM ? 'DM' : 'server message'}:`, { + message: err?.message, + stack: err?.stack, + raw: error, + }); try { await message.reply( 'Sorry, I encountered an error processing your message. Please try again later.', @@ -427,7 +823,7 @@ export default class MessageCreateEvent { } } - private async sendResponse(message: Message, aiResponse: AIResponse): Promise { + private async sendResponse(message: Message, aiResponse: AIResponse): Promise { let fullResponse = ''; if (aiResponse.reasoning) { @@ -436,25 +832,33 @@ export default class MessageCreateEvent { fullResponse += aiResponse.content; + if (!fullResponse || !fullResponse.trim()) { + fullResponse = '\u200b'; + } + const maxLength = 2000; if (fullResponse.length <= maxLength) { - await message.reply({ + const sent = await message.reply({ content: fullResponse, allowedMentions: { parse: ['users'] as const }, }); - } else { - const chunks = splitResponseIntoChunks(fullResponse, maxLength); + return sent; + } + const chunks = splitResponseIntoChunks(fullResponse, maxLength); - await message.reply({ content: chunks[0], allowedMentions: { parse: ['users'] as const } }); + const first = await message.reply({ + content: chunks[0], + allowedMentions: { parse: ['users'] as const }, + }); - for (let i = 1; i < chunks.length; i++) { - if ('send' in message.channel) { - await message.channel.send({ - content: chunks[i], - allowedMentions: { parse: ['users'] as const }, - }); - } + for (let i = 1; i < chunks.length; i++) { + if ('send' in message.channel) { + await message.channel.send({ + content: chunks[i], + allowedMentions: { parse: ['users'] as const }, + }); } } + return first; } } diff --git a/src/events/ready.ts b/src/events/ready.ts index 09b939c..f97512a 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -4,7 +4,7 @@ import { loadActiveReminders } from '@/commands/utilities/remind'; export default class ReadyEvent { constructor(c: BotClient) { - c.once('ready', () => this.readyEvent(c)); + c.once('clientReady', () => this.readyEvent(c)); } private async readyEvent(client: BotClient) { diff --git a/src/index.ts b/src/index.ts index 57c8e39..956a908 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; +import './utils/djsModalPatch'; import BotClient from './services/Client'; import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './config'; import rateLimit from 'express-rate-limit'; diff --git a/src/routes/apiKeys.ts b/src/routes/apiKeys.ts index ee799f3..0fa963a 100644 --- a/src/routes/apiKeys.ts +++ b/src/routes/apiKeys.ts @@ -13,7 +13,7 @@ const ALLOWED_API_HOSTS = [ 'api.anthropic.com', 'api.deepseek.com', 'api.moonshot.ai', - 'api.perplexity.ai' + 'api.perplexity.ai', ]; function getOpenAIClient(apiKey: string, baseURL?: string): OpenAI { diff --git a/src/services/Client.ts b/src/services/Client.ts index 7b2f46e..66040c0 100644 --- a/src/services/Client.ts +++ b/src/services/Client.ts @@ -21,6 +21,8 @@ export default class BotClient extends Client { // eslint-disable-next-line @typescript-eslint/no-explicit-any public t = new Collection(); public socialMediaManager?: SocialMediaManager; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public lastModalRawByUser: Map = new Map(); constructor() { super({ @@ -41,6 +43,42 @@ export default class BotClient extends Client { }, }); BotClient.instance = this; + + interface RawPacket { + t: string; + d?: { + type: number; + member?: { + user: { + id: string; + }; + }; + user?: { + id: string; + }; + }; + } + + this.on('raw', (packet: RawPacket) => { + try { + if (packet?.t !== 'INTERACTION_CREATE') return; + const d = packet.d; + if (d?.type !== 5) return; + const userId = d?.member?.user?.id || d?.user?.id; + if (!userId) return; + this.lastModalRawByUser.set(userId, d); + setTimeout( + () => { + if (this.lastModalRawByUser.get(userId) === d) { + this.lastModalRawByUser.delete(userId); + } + }, + 5 * 60 * 1000, + ); + } catch (e) { + logger.warn('Failed to capture raw modal payload', e); + } + }); } public static getInstance(): BotClient | null { diff --git a/src/types/componentsV2.ts b/src/types/componentsV2.ts new file mode 100644 index 0000000..c18ccde --- /dev/null +++ b/src/types/componentsV2.ts @@ -0,0 +1,222 @@ +export type V2ComponentType = 3 | 4 | 10 | 12 | 18; + +export interface V2BaseComponent { + type: V2ComponentType; +} + +export interface V2StringSelect extends V2BaseComponent { + type: 3; + custom_id: string; + placeholder?: string; + options: Array<{ + label: string; + value: string; + description?: string; + emoji?: { id?: string; name?: string; animated?: boolean }; + }>; + min_values?: number; + max_values?: number; +} + +export interface V2TextInput extends V2BaseComponent { + type: 4; + custom_id: string; + style: 1 | 2; + label?: string; + placeholder?: string; + required?: boolean; + min_length?: number; + max_length?: number; + value?: string; +} + +export interface V2LabeledComponent extends V2BaseComponent { + type: 18; + label?: string; + description?: string; + component: V2StringSelect | V2TextInput; +} + +export type V2ModalRow = + | V2LabeledComponent + | { type: 1; components: V2TextInput[] } + | { type: 18; components: V2TextInput[] }; + +export interface V2SubmissionValueMap { + [customId: string]: string | string[]; +} + +export interface V2ModalPayload { + custom_id: string; + title: string; + components: V2ModalRow[]; +} + +interface V2Component { + type: number; + custom_id?: string; + customId?: string; + value?: string | string[]; + values?: string[]; + [key: string]: unknown; +} + +interface V2Row { + component?: V2Component; + components?: V2Component[]; + type?: number; + [key: string]: unknown; +} + +export function buildProviderModal(customId: string, title: string): V2ModalPayload { + return { + custom_id: customId, + title, + components: [ + { + type: 18, + label: 'AI Provider', + description: 'Select an authorized provider', + component: { + type: 3, + custom_id: 'provider', + placeholder: 'Choose provider', + options: [ + { label: 'OpenAI', value: 'openai', description: 'api.openai.com' }, + { label: 'OpenRouter', value: 'openrouter', description: 'openrouter.ai' }, + { + label: 'Google Gemini', + value: 'gemini', + description: 'generativelanguage.googleapis.com', + }, + { label: 'DeepSeek', value: 'deepseek', description: 'api.deepseek.com' }, + { label: 'Moonshot AI', value: 'moonshot', description: 'api.moonshot.ai' }, + { label: 'Perplexity AI', value: 'perplexity', description: 'api.perplexity.ai' }, + ], + }, + }, + { + type: 1 as const, + components: [ + { + type: 4, + custom_id: 'model', + label: 'Model', + style: 1, + required: true, + placeholder: 'openai/gpt-4o-mini', + min_length: 2, + max_length: 100, + }, + ], + }, + { + type: 1 as const, + components: [ + { + type: 4, + custom_id: 'apiKey', + label: 'API Key', + style: 1, + required: true, + placeholder: 'sk-... or other', + min_length: 10, + max_length: 500, + }, + ], + }, + ], + }; +} + +interface RawModalSubmission { + fields?: { + getTextInputValue?: (id: string) => string; + [key: string]: unknown; + }; + data?: { + components?: Array<{ + component?: V2Component; + components?: V2Component[]; + [key: string]: unknown; + }>; + [key: string]: unknown; + }; + components?: Array<{ + component?: V2Component; + components?: V2Component[]; + [key: string]: unknown; + }>; + message?: { + components?: Array<{ + component?: V2Component; + components?: V2Component[]; + [key: string]: unknown; + }>; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export function parseV2ModalSubmission(raw: RawModalSubmission): V2SubmissionValueMap { + const result: V2SubmissionValueMap = {}; + try { + const fields = raw?.fields as { getTextInputValue?: (id: string) => string } | undefined; + if (fields?.getTextInputValue) { + for (const id of ['model', 'apiKey']) { + try { + const v = fields.getTextInputValue(id); + if (v !== undefined && v !== '') result[id] = v; + } catch (error) { + console.error(`Error getting text input value for ${id}:`, error); + } + } + } + } catch (error) { + console.error('Error processing text input fields:', error); + } + + try { + const mergedRows = ( + [ + ...(raw?.data?.components || []), + ...(raw?.components || []), + ...(raw?.message?.components || []), + ] as Array + ).filter(Boolean) as V2Row[]; + + const flat = mergedRows.flatMap((r) => (r.component ? [r.component] : r.components || [])); + + for (const c of flat) { + if (!c) continue; + + if (c.customId === 'provider' || c.custom_id === 'provider') { + if (Array.isArray((c as { values?: string[] }).values)) { + result.provider = (c as { values: string[] }).values; + } else if ((c as { value?: string | string[] }).value) { + const value = (c as { value: string | string[] }).value; + result.provider = Array.isArray(value) ? value : [value]; + } + } + + if (c.type === 4 && (c.custom_id || c.customId) && (c as { value?: unknown }).value) { + const value = (c as { value: unknown }).value; + if (typeof value === 'string') { + result[c.custom_id || c.customId!] = value; + } + } + } + } catch (error) { + console.error('Error processing component values:', error); + } + return result; +} + +export const PROVIDER_TO_URL: Record = { + openai: 'https://api.openai.com/v1', + openrouter: 'https://openrouter.ai/api/v1', + gemini: 'https://generativelanguage.googleapis.com', + deepseek: 'https://api.deepseek.com', + moonshot: 'https://api.moonshot.ai', + perplexity: 'https://api.perplexity.ai', +}; diff --git a/src/utils/commandExecutor.ts b/src/utils/commandExecutor.ts new file mode 100644 index 0000000..bc66ffe --- /dev/null +++ b/src/utils/commandExecutor.ts @@ -0,0 +1,692 @@ +import { + ChatInputCommandInteraction, + InteractionReplyOptions, + MessagePayload, + EmbedBuilder, +} from 'discord.js'; +import { SlashCommandProps } from '@/types/command'; +import BotClient from '@/services/Client'; +import logger from '@/utils/logger'; + +export interface ToolCall { + name: string; + args: Record; +} + +type EmbedInput = Record & { + data?: Record; + toJSON?: () => unknown; + title?: unknown; + description?: unknown; + fields?: unknown; + color?: unknown; + timestamp?: unknown; + footer?: unknown; +}; + +function toPlainEmbedObject(embed: unknown): Record | unknown { + if (embed && typeof embed === 'object') { + const embedObj = embed as EmbedInput; + if ('data' in embedObj && embedObj.data) { + return embedObj.data as Record; + } + if (typeof embedObj.toJSON === 'function') { + return embedObj.toJSON(); + } + const e = embedObj as Record; + if ('title' in e || 'fields' in e || 'description' in e) { + return e; + } + return { + title: embedObj.title, + description: embedObj.description, + fields: embedObj.fields, + color: embedObj.color, + timestamp: embedObj.timestamp, + footer: embedObj.footer, + } as Record; + } + return embed as Record; +} + +function testEmbedBuilderStructure() { + const testEmbed = new EmbedBuilder() + .setTitle('Test Title') + .setDescription('Test Description') + .addFields({ name: 'Test Field', value: 'Test Value', inline: true }); + + logger.debug(`[Command Executor] Test EmbedBuilder structure:`, { + embed: testEmbed, + hasData: 'data' in testEmbed, + data: testEmbed.data, + hasToJSON: typeof testEmbed.toJSON === 'function', + toJSON: testEmbed.toJSON ? testEmbed.toJSON() : 'N/A', + keys: Object.keys(testEmbed), + prototype: Object.getPrototypeOf(testEmbed)?.constructor?.name, + }); + + if (testEmbed.data) { + logger.debug(`[Command Executor] Test embed data fields:`, { + fields: testEmbed.data.fields, + fieldsLength: testEmbed.data.fields?.length, + allDataKeys: Object.keys(testEmbed.data), + }); + } +} + +export function extractToolCalls(content: string): { cleanContent: string; toolCalls: ToolCall[] } { + const toolCallRegex = /{([^{}\s:]+):({[^{}]*}|[^{}]*)?}/g; + const toolCalls: ToolCall[] = []; + let cleanContent = content; + let match; + + while ((match = toolCallRegex.exec(content)) !== null) { + try { + if (!match[1]) { + continue; + } + + const toolName = match[1].trim(); + const argsString = match[2] ? match[2].trim() : ''; + + if (!toolName) { + continue; + } + + let args: Record = {}; + + if (argsString.startsWith('{') && argsString.endsWith('}')) { + try { + args = JSON.parse(argsString); + } catch (_error) { + args = { query: argsString }; + } + } else if (argsString) { + if (argsString.startsWith('"') && argsString.endsWith('"')) { + const unquoted = argsString.slice(1, -1); + if (toolName === 'reaction') { + args = { emoji: unquoted }; + } else { + args = { query: unquoted }; + } + } else { + args = { query: argsString }; + } + } else { + args = {}; + } + + toolCalls.push({ + name: toolName, + args, + }); + + cleanContent = cleanContent.replace(match[0], '').trim(); + } catch (error) { + logger.error(`Error parsing tool call: ${error}`); + } + } + + return { cleanContent, toolCalls }; +} + +export async function executeToolCall( + toolCall: ToolCall, + interaction: ChatInputCommandInteraction, + client: BotClient, +): Promise { + let { name, args } = toolCall; + + if (name.includes(':')) { + const parts = name.split(':'); + name = parts[0]; + if (!args || Object.keys(args).length === 0) { + args = { search: parts[1] }; + } + } + + try { + const validCommands = ['cat', 'dog', 'joke', '8ball', 'weather', 'wiki']; + + if (!validCommands.includes(name.toLowerCase())) { + throw new Error( + `Command '${name}' is not a valid command. Available commands: ${validCommands.join(', ')}`, + ); + } + + if (['cat', 'dog'].includes(name)) { + const commandName = name.charAt(0).toUpperCase() + name.slice(1); + logger.debug(`[${commandName}] Starting ${name} command execution`); + + try { + const commandDir = 'fun'; + const commandModule = await import(`../commands/${commandDir}/${name}`); + + const imageData = + name === 'cat' + ? await commandModule.fetchCatImage() + : await commandModule.fetchDogImage(); + + return JSON.stringify({ + success: true, + type: name, + title: imageData.title || `Random ${commandName}`, + url: imageData.url, + subreddit: imageData.subreddit, + source: name === 'cat' ? 'pur.cat' : 'erm.dog', + handled: true, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : `Unknown error in ${name} command`; + logger.error(`[${commandName}] Error: ${errorMessage}`, { error }); + return JSON.stringify({ + success: false, + error: `Failed to execute ${name} command: ${errorMessage}`, + handled: false, + }); + } + } + + if (name === 'wiki') { + logger.debug('[Wiki] Starting wiki command execution'); + + try { + const wikiModule = await import('../commands/utilities/wiki'); + + let searchQuery = ''; + if (typeof args === 'object' && args !== null) { + const argsObj = args as Record; + searchQuery = (argsObj.search as string) || (argsObj.query as string) || ''; + } + + if (!searchQuery) { + return JSON.stringify({ + error: true, + message: 'Missing search query', + status: 400, + }); + } + + try { + logger.debug( + `[Wiki] Searching Wikipedia for: ${searchQuery} (locale: ${interaction.locale || 'en'})`, + ); + + const searchResult = await wikiModule.searchWikipedia( + searchQuery, + interaction.locale || 'en', + ); + logger.debug(`[Wiki] Search result:`, { + pageid: searchResult.pageid, + title: searchResult.title, + }); + + const article = await wikiModule.getArticleSummary( + searchResult.pageid, + searchResult.wikiLang, + ); + logger.debug(`[Wiki] Retrieved article:`, { + title: article.title, + extractLength: article.extract?.length, + }); + + const maxLength = 1500; + const truncated = article.extract && article.extract.length > maxLength; + const extract = truncated + ? article.extract.substring(0, maxLength) + : article.extract || 'No summary available for this article.'; + + const response = { + title: article.title, + content: extract, + url: `https://${searchResult.wikiLang}.wikipedia.org/wiki/${encodeURIComponent(article.title.replace(/ /g, '_'))}`, + truncated: truncated, + }; + + return JSON.stringify(response); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const stack = error instanceof Error ? error.stack : undefined; + const responseStatus = + error instanceof Error && 'response' in error + ? (error as { response?: { status?: number } }).response?.status + : undefined; + + logger.error('[Wiki] Error executing wiki command:', { + error: errorMessage, + stack, + responseStatus, + searchQuery, + locale: interaction.locale, + }); + + return JSON.stringify({ + error: true, + message: + responseStatus === 404 + ? 'No Wikipedia article found for that search query. Please try a different search term.' + : `Error searching Wikipedia: ${errorMessage}`, + status: responseStatus || 500, + }); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const stack = error instanceof Error ? error.stack : undefined; + logger.error('[Wiki] Error in wiki command execution:', { error: errorMessage, stack }); + + return JSON.stringify({ + error: true, + message: `Error in wiki command execution: ${errorMessage}`, + status: 500, + }); + } + } + + let commandModule; + const isDev = process.env.NODE_ENV !== 'production'; + const ext = isDev ? '.ts' : '.js'; + + try { + let commandDir = 'fun'; + if ( + ['weather', 'wiki', 'ai', 'cobalt', 'remind', 'social', 'time', 'todo', 'whois'].includes( + name, + ) + ) { + commandDir = 'utilities'; + } + + const commandPath = `../commands/${commandDir}/${name}${ext}`; + logger.debug(`[Command Executor] Trying to import command from: ${commandPath}`); + commandModule = await import(commandPath).catch((e) => { + logger.error(`[Command Executor] Error importing command '${name}':`, e); + throw e; + }); + + if (name === 'cat' || name === 'dog') { + try { + const imageData = + name === 'cat' + ? await commandModule.fetchCatImage() + : await commandModule.fetchDogImage(); + + return JSON.stringify({ + success: true, + type: name, + title: imageData.title || `Random ${name === 'cat' ? 'Cat' : 'Dog'}`, + url: imageData.url, + subreddit: imageData.subreddit, + source: name === 'cat' ? 'pur.cat' : 'erm.dog', + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : `Unknown error fetching ${name} image`; + logger.error(`[${name}] Error: ${errorMessage}`, { error }); + return JSON.stringify({ + success: false, + error: `Failed to fetch ${name} image: ${errorMessage}`, + }); + } + } + } catch (error) { + logger.error(`[Command Executor] Error importing command '${name}':`, error); + throw new Error(`Command '${name}' not found`); + } + + const command = commandModule.default as SlashCommandProps; + + if (!command) { + throw new Error(`Command '${name}' not found`); + } + + let capturedResponse: unknown = null; + + const mockInteraction = { + ...interaction, + options: { + getString: (param: string) => { + if (typeof args === 'object' && args !== null) { + const argsObj = args as Record; + return (argsObj[param] as string) || ''; + } + return ''; + }, + getNumber: (param: string) => { + if (typeof args === 'object' && args !== null) { + const argsObj = args as Record; + const value = argsObj[param]; + return value !== null && value !== undefined ? Number(value) : null; + } + return null; + }, + getBoolean: (param: string) => { + if (typeof args === 'object' && args !== null) { + const argsObj = args as Record; + const value = argsObj[param]; + return typeof value === 'boolean' ? value : null; + } + return null; + }, + }, + deferReply: async () => { + return Promise.resolve(); + }, + reply: async (options: InteractionReplyOptions | MessagePayload) => { + if ('embeds' in options && options.embeds && options.embeds.length > 0) { + const processedEmbeds = options.embeds.map((e) => toPlainEmbedObject(e)); + capturedResponse = { ...(options as Record), embeds: processedEmbeds }; + } else { + capturedResponse = options; + } + if ('embeds' in options && options.embeds && options.embeds.length > 0) { + return JSON.stringify({ + success: true, + embeds: + 'embeds' in (capturedResponse as Record) + ? (capturedResponse as { embeds?: unknown[] }).embeds + : options.embeds, + }); + } + return JSON.stringify({ + success: true, + content: 'content' in options ? options.content : undefined, + }); + }, + editReply: async (options: InteractionReplyOptions | MessagePayload) => { + if ('embeds' in options && options.embeds && options.embeds.length > 0) { + const processedEmbeds = options.embeds.map((e) => toPlainEmbedObject(e)); + capturedResponse = { ...(options as Record), embeds: processedEmbeds }; + } else { + capturedResponse = options; + } + logger.debug(`[Command Executor] editReply called with options:`, { + hasEmbeds: 'embeds' in options && options.embeds && options.embeds.length > 0, + hasContent: 'content' in options, + options: options, + }); + + if ('embeds' in options && options.embeds && options.embeds.length > 0) { + logger.debug(`[Command Executor] Raw embeds before processing:`, options.embeds); + + testEmbedBuilderStructure(); + + const processedEmbeds = options.embeds.map((embed) => { + const embedObj = embed as EmbedInput; + logger.debug(`[Command Executor] Processing embed:`, { + hasData: !!(embedObj && typeof embedObj === 'object' && 'data' in embedObj), + embedKeys: embedObj ? Object.keys(embedObj) : [], + embedType: (embed as { constructor?: { name?: string } })?.constructor?.name, + embedPrototype: Object.getPrototypeOf(embedObj as object)?.constructor?.name, + }); + const plain = toPlainEmbedObject(embedObj); + return plain; + }); + + logger.debug(`[Command Executor] Processed embeds:`, processedEmbeds); + + if (processedEmbeds.length > 0) { + const firstEmbed = processedEmbeds[0]; + const firstEmbedObj = firstEmbed as Record; + logger.debug(`[Command Executor] First processed embed details:`, { + title: (firstEmbedObj as { title?: unknown })?.title, + description: (firstEmbedObj as { description?: unknown })?.description, + fields: (firstEmbedObj as { fields?: unknown })?.fields, + fieldsLength: (firstEmbedObj as { fields?: Array })?.fields?.length, + allKeys: firstEmbedObj ? Object.keys(firstEmbedObj) : [], + }); + + logger.debug( + `[Command Executor] Full embed structure:`, + JSON.stringify(firstEmbedObj, null, 2), + ); + } + + const response = { + success: true, + embeds: processedEmbeds, + }; + logger.debug(`[Command Executor] Returning embed response:`, response); + return JSON.stringify(response); + } + const response = { + success: true, + content: 'content' in options ? options.content : undefined, + }; + logger.debug(`[Command Executor] Returning content response:`, response); + return JSON.stringify(response); + }, + followUp: async (options: InteractionReplyOptions | MessagePayload) => { + capturedResponse = options; + return JSON.stringify({ + success: true, + content: 'content' in options ? options.content : undefined, + }); + }, + } as unknown as ChatInputCommandInteraction; + + try { + const result = await command.execute(client, mockInteraction); + + const responseToProcess = capturedResponse || result; + + logger.debug(`[Command Executor] Processing response for ${name}:`, { + hasCapturedResponse: !!capturedResponse, + hasResult: result !== undefined, + responseType: typeof responseToProcess, + }); + + if (!responseToProcess) { + return JSON.stringify({ + success: true, + message: 'Command executed successfully', + }); + } + + if (typeof responseToProcess === 'string') { + return JSON.stringify({ + success: true, + content: responseToProcess, + }); + } + + if (typeof responseToProcess === 'object') { + const response = responseToProcess as Record; + + if (Array.isArray(response.embeds) && response.embeds.length > 0) { + const embeds = response.embeds as Array<{ + title?: string; + description?: string; + fields?: Array<{ name: string; value: string; inline?: boolean }>; + color?: number; + timestamp?: string | number | Date; + footer?: { text: string; icon_url?: string }; + }>; + + logger.debug(`[Command Executor] Processing embeds for ${name}:`, { + embedCount: embeds.length, + firstEmbed: embeds[0], + embedFields: embeds[0]?.fields, + embedTitle: embeds[0]?.title, + }); + + if (name === 'weather') { + const embed = embeds[0]; + logger.debug(`[Command Executor] Weather embed details:`, { + hasEmbed: !!embed, + hasFields: !!(embed && embed.fields), + fieldsCount: embed?.fields?.length || 0, + embedData: embed, + }); + + if (embed && embed.fields && embed.fields.length > 0) { + const f = embed.fields; + const title = embed.title || ''; + let locationFromTitle = ''; + try { + const m = title.match(/([A-Za-zΓ€-Γ–Γ˜-ΓΆΓΈ-ΓΏ' .-]+,\s*[A-Z]{2})/u); + if (m && m[1]) { + locationFromTitle = m[1].trim(); + } + } catch (_err) { + /* ignore */ + } + let locationArg = ''; + if (args && typeof args === 'object') { + const a = args as Record; + locationArg = String(a.location || a.query || a.search || '').trim(); + } + const weatherResponse = { + success: true, + type: 'weather', + location: locationFromTitle || locationArg || 'Unknown location', + temperature: f[0]?.value || 'N/A', + feels_like: f[1]?.value || 'N/A', + conditions: f[2]?.value || 'N/A', + humidity: f[3]?.value || 'N/A', + wind_speed: f[4]?.value || 'N/A', + pressure: f[5]?.value || 'N/A', + handled: true, + }; + logger.debug(`[Command Executor] Weather response:`, weatherResponse); + return JSON.stringify(weatherResponse); + } + logger.debug(`[Command Executor] Weather embed has no fields, using fallback`); + logger.debug(`[Command Executor] Weather embed fallback data:`, { + title: embed?.title, + description: embed?.description, + fields: embed?.fields, + allProperties: embed ? Object.keys(embed) : [], + }); + + const fallbackResponse = { + success: true, + type: 'weather', + location: + embed?.title || + (args && typeof args === 'object' + ? String( + (args as Record).location || + (args as Record).query || + (args as Record).search || + '', + ) + : '') || + 'Unknown location', + description: embed?.description || 'Weather data unavailable', + rawEmbed: embed, + handled: true, + }; + + return JSON.stringify(fallbackResponse); + } + + if (name === 'joke') { + const embed = embeds[0]; + return JSON.stringify({ + success: true, + type: 'joke', + title: embed.title || 'Random Joke', + setup: embed.description || 'No joke available', + handled: true, + }); + } + + return JSON.stringify({ + success: true, + embeds: embeds.map((embed) => ({ + title: embed.title, + description: embed.description, + fields: + embed.fields?.map((f) => ({ + name: f.name, + value: f.value, + inline: f.inline, + })) || [], + color: embed.color, + timestamp: embed.timestamp, + footer: embed.footer, + })), + }); + } + + if (Array.isArray(response.components) && response.components.length > 0) { + if (name === '8ball') { + const components = response.components as Array<{ + components?: Array<{ + data?: { + components?: Array<{ + data?: { + content?: string; + }; + }>; + }; + }>; + }>; + + let question = ''; + let answer = ''; + + for (const component of components) { + if (component.components) { + for (const subComponent of component.components) { + if (subComponent.data?.components) { + for (const textComponent of subComponent.data.components) { + const content = textComponent.data?.content || ''; + if (content.includes('**Question**') || content.includes('**Pregunta**')) { + question = content + .replace(/.*?\*\*(.*?)\*\*\s*>\s*(.*?)\n\n.*/s, '$2') + .trim(); + } else if ( + content.includes('**Answer**') || + content.includes('**Respuesta**') || + content.includes('✨') + ) { + answer = content.replace(/.*?✨\s*(.*?)$/s, '$1').trim(); + } + } + } + } + } + } + + return JSON.stringify({ + success: true, + type: '8ball', + question: question || 'Unknown question', + answer: answer || 'Unknown answer', + handled: true, + }); + } + } + + if (typeof response.content === 'string') { + return JSON.stringify({ + success: true, + content: response.content, + }); + } + } + + return JSON.stringify({ + success: true, + message: 'Command executed successfully', + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + logger.error(`Error executing command '${name}':`, errorMessage); + return JSON.stringify({ + success: false, + error: errorMessage, + status: 500, + }); + } + } catch (error) { + logger.error(`Error executing tool call '${name}':`, error); + return JSON.stringify({ + success: false, + error: `Error executing tool call '${name}': ${error}`, + status: 500, + }); + } +} diff --git a/src/utils/djsModalPatch.ts b/src/utils/djsModalPatch.ts new file mode 100644 index 0000000..58fd55a --- /dev/null +++ b/src/utils/djsModalPatch.ts @@ -0,0 +1,77 @@ +import Module from 'module'; +import path from 'path'; + +class SafeModalSubmitFields { + public components: unknown[] = []; + + public fields: { + getField: (customId: string) => { value: string }; + }; + + private fieldsMap: Record = {}; + + constructor(components: unknown) { + this.components = Array.isArray(components) ? components : []; + + try { + const rows = this.components as Array>; + for (const row of rows) { + const labeled = row?.component ? [row.component] : undefined; + const innerComponents = Array.isArray(row?.components) ? row.components : labeled || []; + + for (const comp of innerComponents) { + const leaf = comp?.component || comp; + const isTextInput = leaf?.type === 4 || typeof leaf?.style === 'number'; + if (!isTextInput) continue; + const id = leaf?.custom_id || leaf?.customId || leaf?.id; + if (!id) continue; + const value = typeof leaf?.value === 'string' ? leaf.value : ''; + this.fieldsMap[id] = value; + } + } + } catch { + // ignore + } + + this.fields = { + getField: (customId: string) => { + if (!(customId in this.fieldsMap)) { + throw new Error(`Text input with custom id '${customId}' not found`); + } + return { value: this.fieldsMap[customId] }; + }, + }; + } + + getTextInputValue(customId: string): string { + return this.fields.getField(customId).value; + } +} + +try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anyModule = Module as any; + const originalLoad = anyModule._load; + anyModule._load = function ( + this: unknown, + request: string, + parent: unknown, + _isMain: boolean, + ...args: unknown[] + ) { + try { + const resolved = anyModule._resolveFilename(request, parent); + if (typeof resolved === 'string') { + const normalized = path.posix.normalize(resolved.replace(/\\/g, '/')); + if (normalized.includes('/structures/ModalSubmitFields.js')) { + return SafeModalSubmitFields; + } + } + } catch { + // fallthrough + } + return originalLoad.apply(this, [request, parent, _isMain, ...args] as const); + }; +} catch { + // ignore +} diff --git a/src/utils/messageToolExecutor.ts b/src/utils/messageToolExecutor.ts new file mode 100644 index 0000000..c8caf19 --- /dev/null +++ b/src/utils/messageToolExecutor.ts @@ -0,0 +1,124 @@ +import { Message } from 'discord.js'; +import BotClient from '@/services/Client'; +import logger from '@/utils/logger'; + +export interface MessageToolCall { + name: string; + args: Record; +} + +function extractEmojiArg(args: Record): string { + const raw = (args.emoji as string) || (args.query as string) || ''; + return typeof raw === 'string' ? raw.trim() : ''; +} + +function normalizeEmoji(input: string): string { + if (!input) return ''; + const customMatch = input.match(//); + if (customMatch && customMatch[1]) { + return customMatch[0]; + } + const shortcode = input.match(/^:([a-z0-9_+-]+):$/i)?.[1]; + if (shortcode) { + const map: Record = { + thumbsup: 'πŸ‘', + thumbsdown: 'πŸ‘Ž', + '+1': 'πŸ‘', + '-1': 'πŸ‘Ž', + thumbs_up: 'πŸ‘', + thumbs_down: 'πŸ‘Ž', + heart: '❀️', + smile: 'πŸ˜„', + grin: '😁', + joy: 'πŸ˜‚', + cry: '😒', + sob: '😭', + clap: 'πŸ‘', + fire: 'πŸ”₯', + star: '⭐', + eyes: 'πŸ‘€', + tada: 'πŸŽ‰', + }; + const key = shortcode.toLowerCase(); + return map[key] || input; + } + return input; +} + +export async function executeMessageToolCall( + toolCall: MessageToolCall, + message: Message, + _client: BotClient, + opts?: { originalMessage?: Message; botMessage?: Message }, +): Promise<{ success: boolean; type: string; handled: boolean; error?: string }> { + const name = (toolCall.name || '').toLowerCase(); + try { + if (name === 'reaction') { + const emojiRaw = extractEmojiArg(toolCall.args || {}); + const emoji = normalizeEmoji(emojiRaw); + if (!emoji) { + return { success: false, type: 'reaction', handled: false, error: 'Missing emoji' }; + } + + let targetMsg: Message = message; + try { + const targetSpec = (toolCall.args?.target as string) || ''; + const msgId = (toolCall.args?.target_message_id as string) || ''; + const url = (toolCall.args?.target_url as string) || ''; + + if (targetSpec === 'user' && opts?.originalMessage) { + targetMsg = opts.originalMessage; + } else if (targetSpec === 'bot' && opts?.botMessage) { + targetMsg = opts.botMessage; + } else if (msgId && message.channel && 'messages' in message.channel) { + try { + const fetched = await message.channel.messages.fetch(msgId); + if (fetched) targetMsg = fetched; + } catch (error) { + logger.warn(`Failed to fetch message with ID ${msgId}:`, error); + } + } else if (url) { + const m = url.match(/discord\.com\/channels\/\d+\/(\d+)\/(\d+)/); + const channelId = m?.[1]; + const messageId = m?.[2]; + try { + if (channelId && messageId && message.client.channels) { + const ch = await message.client.channels.fetch(channelId); + if (ch && 'messages' in ch) { + const fetched = await ch.messages.fetch(messageId); + if (fetched) targetMsg = fetched; + } + } + } catch (error) { + logger.warn(`Failed to fetch message from URL ${url}:`, error); + } + } + } catch (error) { + logger.warn('Error processing message target:', error); + } + + try { + await targetMsg.react(emoji); + return { success: true, type: 'reaction', handled: true }; + } catch (_err) { + try { + await targetMsg.react(emojiRaw); + return { success: true, type: 'reaction', handled: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error('[MessageToolExecutor] Failed to add reaction:', { + emoji: emojiRaw, + error: errorMessage, + }); + return { success: false, type: 'reaction', handled: false, error: errorMessage }; + } + } + } + + return { success: false, type: name || 'unknown', handled: false, error: 'Unknown tool' }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error('[MessageToolExecutor] Error executing tool call:', { name, error: errorMessage }); + return { success: false, type: name || 'unknown', handled: false, error: errorMessage }; + } +}