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

Compare changes

Choose any two refs to compare.

+13 -6
.env.example
··· 2 2 ALLOWED_ORIGINS= 3 3 # Encryption secret for data stored in the Database (Like API keys) 4 4 API_KEY_ENCRYPTION_SECRET= 5 - # Client ID found in the Oauth page of the Discord Developer Portal 5 + # Discord bot credentials, can be found in https://discord.com/developers/applications 6 6 CLIENT_ID= 7 + CLIENT_SECRET= 8 + REDIRECT_URI=http://localhost:3000/api/auth/discord/callback 9 + TOKEN= 7 10 # Postgresql database URLs 8 11 DATABASE_URL= 9 12 # Openrouter API key for the default AI model 10 13 OPENROUTER_API_KEY= 11 14 # OpenWeather API key for the weather command 12 15 OPENWEATHER_API_KEY= 16 + # Massive.com API key for the /stocks command 17 + MASSIVE_API_KEY= 18 + # Optional override for the Massive.com REST base URL 19 + MASSIVE_API_BASE_URL=https://api.massive.com 13 20 # Log level for the application (debug, info, warn, error) 14 21 LOG_LEVEL=info 15 22 # Node environment (development, production) ··· 18 25 SOURCE_COMMIT= 19 26 # The API key going to be used for the status API 20 27 STATUS_API_KEY= 21 - # The bot's token 22 - TOKEN= 23 28 # Frontend envs 24 29 VITE_BOT_API_URL= 25 30 VITE_STATUS_API_KEY= 26 - VITE_FRONTEND_URL= 27 - VITE_DISCORD_CLIENT_ID= 28 - 29 31 # Deployment notification webhook 30 32 DEPLOYMENT_WEBHOOK_URL= 33 + # Top.gg Webhook Configuration 34 + TOPGG_WEBHOOK_SECRET= 35 + TOPGG_WEBHOOK_AUTH= 36 + # Top.gg token to check vote status 37 + TOPGG_TOKEN=
+1
.github/FUNDING.yml
··· 1 + ko_fi: scanash
+2 -2
.github/workflows/pr.yml
··· 8 8 jobs: 9 9 pr-checks: 10 10 name: PR Quality Checks 11 - runs-on: self-hosted 11 + runs-on: ubuntu-latest 12 12 permissions: 13 13 contents: read 14 14 pull-requests: read ··· 70 70 71 71 size-check: 72 72 name: Bundle Size Check 73 - runs-on: self-hosted 73 + runs-on: ubuntu-latest 74 74 permissions: 75 75 contents: read 76 76 pull-requests: read
+2 -10
Dockerfile
··· 3 3 ARG SOURCE_COMMIT 4 4 ARG VITE_BOT_API_URL 5 5 ARG VITE_STATUS_API_KEY 6 - ARG VITE_FRONTEND_URL 7 - ARG VITE_DISCORD_CLIENT_ID 8 6 ARG STATUS_API_KEY 9 7 10 8 ENV SOURCE_COMMIT=${SOURCE_COMMIT} 11 9 ENV NODE_ENV=production 12 10 ENV VITE_BOT_API_URL=${VITE_BOT_API_URL} 13 11 ENV VITE_STATUS_API_KEY=${VITE_STATUS_API_KEY} 14 - ENV VITE_FRONTEND_URL=${VITE_FRONTEND_URL} 15 - ENV VITE_DISCORD_CLIENT_ID=${VITE_DISCORD_CLIENT_ID} 16 12 ENV STATUS_API_KEY=${STATUS_API_KEY} 17 13 18 14 WORKDIR /app 19 15 20 16 21 - 17 + RUN apt-get update && apt-get install -y git fonts-dejavu-core fontconfig && rm -rf /var/lib/apt/lists/* 22 18 COPY package.json bun.lock ./ 23 19 RUN bun install --frozen-lockfile 24 20 ··· 52 48 ARG SOURCE_COMMIT 53 49 ARG VITE_BOT_API_URL 54 50 ARG VITE_STATUS_API_KEY 55 - ARG VITE_FRONTEND_URL 56 - ARG VITE_DISCORD_CLIENT_ID 57 51 58 52 ENV SOURCE_COMMIT=${SOURCE_COMMIT} 59 53 ENV NODE_ENV=production 60 54 ENV VITE_BOT_API_URL=${VITE_BOT_API_URL} 61 55 ENV STATUS_API_KEY=${STATUS_API_KEY} 62 56 ENV VITE_STATUS_API_KEY=${VITE_STATUS_API_KEY} 63 - ENV VITE_FRONTEND_URL=${VITE_FRONTEND_URL} 64 - ENV VITE_DISCORD_CLIENT_ID=${VITE_DISCORD_CLIENT_ID} 65 57 66 - RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* && \ 58 + RUN apt-get update && apt-get install -y curl git fonts-dejavu-core fontconfig && rm -rf /var/lib/apt/lists/* && \ 67 59 groupadd -g 1001 nodejs && \ 68 60 useradd -r -u 1001 -g nodejs aethel 69 61
+1 -6
README.md
··· 51 51 Run all SQL migrations: 52 52 53 53 ```sh 54 - bun run scripts/run-migration.js # or node scripts/run-migration.js 54 + bun run scripts/run-migrations.js # or node scripts/run-migrations.js 55 55 ``` 56 56 57 57 --- ··· 86 86 This project is licensed under the MIT License. 87 87 88 88 See [LICENSE](LICENSE) for details. 89 - 90 - 7. Start the bot: 91 - ```bash 92 - bun start 93 - ``` 94 89 95 90 ## Usage 96 91
+215 -50
bun.lock
··· 4 4 "": { 5 5 "name": "aethel", 6 6 "dependencies": { 7 - "@atproto/identity": "^0.4.8", 8 - "@discordjs/rest": "^2.5.1", 9 - "@fedify/fedify": "^1.1.0", 7 + "@atproto/identity": "^0.4.9", 8 + "@discordjs/rest": "^2.6.0", 9 + "@fedify/fedify": "^1.8.12", 10 + "@massive.com/client-js": "^9.0.0", 10 11 "@types/he": "^1.2.3", 11 12 "@types/sanitize-html": "^2.16.0", 12 - "axios": "^1.11.0", 13 - "city-timezones": "^1.3.1", 13 + "axios": "^1.12.2", 14 + "canvas": "^3.2.0", 15 + "city-timezones": "^1.3.2", 16 + "concurrently": "^9.2.1", 14 17 "cors": "^2.8.5", 15 - "discord.js": "^14.21.0", 18 + "discord.js": "^14.22.1", 16 19 "dotenv": "^16.6.1", 17 20 "eslint-plugin-prettier": "^5.5.4", 18 21 "express": "^4.21.2", ··· 24 27 "moment-timezone": "^0.6.0", 25 28 "node-fetch": "^3.3.2", 26 29 "open-graph-scraper": "^6.10.0", 27 - "openai": "^5.12.2", 30 + "openai": "^5.23.1", 28 31 "pg": "^8.16.3", 29 32 "sanitize-html": "^2.17.0", 30 33 "uuid": "^11.1.0", ··· 33 36 "winston": "^3.17.0", 34 37 }, 35 38 "devDependencies": { 36 - "@eslint/js": "^9.33.0", 39 + "@eslint/js": "^9.36.0", 37 40 "@types/cors": "^2.8.19", 38 41 "@types/express": "^4.17.23", 39 42 "@types/jsonwebtoken": "^9.0.10", 40 - "@types/node": "^24.2.1", 43 + "@types/node": "^24.5.2", 41 44 "@types/open-graph-scraper": "^5.2.3", 42 45 "@types/pg": "^8.15.5", 43 46 "@types/uuid": "^10.0.0", 44 - "@types/validator": "^13.15.2", 47 + "@types/validator": "^13.15.3", 45 48 "@types/whois-json": "^2.0.4", 46 - "eslint": "^9.33.0", 49 + "eslint": "^9.36.0", 47 50 "eslint-config-prettier": "^10.1.8", 48 - "globals": "^16.3.0", 51 + "globals": "^16.4.0", 49 52 "nodemon": "^3.1.10", 50 53 "prettier": "^3.6.2", 51 54 "tsc-alias": "^1.8.16", 52 55 "tsconfig-paths": "^4.2.0", 53 - "tsx": "^4.20.3", 56 + "tsx": "^4.20.6", 54 57 "typescript": "^5.9.2", 55 - "typescript-eslint": "^8.39.0", 58 + "typescript-eslint": "^8.44.1", 56 59 }, 57 60 }, 58 61 }, 59 62 "packages": { 60 - "@atproto/common-web": ["@atproto/common-web@0.4.2", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw=="], 63 + "@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="], 61 64 62 65 "@atproto/crypto": ["@atproto/crypto@0.4.4", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA=="], 63 66 64 - "@atproto/identity": ["@atproto/identity@0.4.8", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/crypto": "^0.4.4" } }, "sha512-Z0sLnJ87SeNdAifT+rqpgE1Rc3layMMW25gfWNo4u40RGuRODbdfAZlTwBSU2r+Vk45hU+iE+xeQspfednCEnA=="], 67 + "@atproto/identity": ["@atproto/identity@0.4.9", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/crypto": "^0.4.4" } }, "sha512-pRYCaeaEJMZ4vQlRQYYTrF3cMiRp21n/k/pUT1o7dgKby56zuLErDmFXkbKfKWPf7SgWRgamSaNmsGLqAOD7lQ=="], 65 68 66 69 "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], 67 70 ··· 77 80 78 81 "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], 79 82 80 - "@discordjs/rest": ["@discordjs/rest@2.5.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw=="], 83 + "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], 81 84 82 85 "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 83 86 ··· 135 138 136 139 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], 137 140 138 - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], 141 + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], 139 142 140 143 "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], 141 144 ··· 147 150 148 151 "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], 149 152 150 - "@eslint/js": ["@eslint/js@9.34.0", "", {}, "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw=="], 153 + "@eslint/js": ["@eslint/js@9.36.0", "", {}, "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw=="], 151 154 152 155 "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], 153 156 ··· 155 158 156 159 "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], 157 160 158 - "@fedify/fedify": ["@fedify/fedify@1.8.8", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@hugoalh/http-header-link": "^1.0.2", "@js-temporal/polyfill": "^0.5.1", "@logtape/logtape": "^1.0.0", "@multiformats/base-x": "^4.0.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@phensley/language-tag": "^1.9.0", "asn1js": "^3.0.5", "byte-encodings": "^1.0.11", "es-toolkit": "^1.39.5", "json-canon": "^1.0.1", "jsonld": "^8.3.2", "multicodec": "^3.2.1", "pkijs": "^3.2.4", "structured-field-values": "^2.0.4", "uri-template-router": "^0.0.17", "url-template": "^3.1.1", "urlpattern-polyfill": "^10.1.0" } }, "sha512-1w5YfKbh8wNbcJ1s0Ttb2l3i4oiWmQ7z5sbKqQJMhBW6odaGGVgEO2bl6ZI5Tg0i5o/LK3ODOKLTlr8F1wibHA=="], 161 + "@fedify/fedify": ["@fedify/fedify@1.8.12", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@hugoalh/http-header-link": "^1.0.2", "@js-temporal/polyfill": "^0.5.1", "@logtape/logtape": "^1.0.0", "@multiformats/base-x": "^4.0.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@phensley/language-tag": "^1.9.0", "asn1js": "^3.0.5", "byte-encodings": "^1.0.11", "es-toolkit": "^1.39.5", "json-canon": "^1.0.1", "jsonld": "^8.3.2", "multicodec": "^3.2.1", "pkijs": "^3.2.4", "structured-field-values": "^2.0.4", "uri-template-router": "^0.0.17", "url-template": "^3.1.1", "urlpattern-polyfill": "^10.1.0" } }, "sha512-bdQIfXDtfzvuiftNarOxpd1VPa09lLzDj0bGXktBFEKKM/C5D0DQTSUr+1NHTP8PFgfIqhcCb93csywP9CsaOA=="], 159 162 160 163 "@hugoalh/http-header-link": ["@hugoalh/http-header-link@1.0.3", "", { "dependencies": { "@hugoalh/is-string-singleline": "^1.0.4" } }, "sha512-x4jzzKSzZQY115H/GxUWaAHzT5eqLXt99uSKY7+0O/h3XrV248+CkZA7cA274QahXzWkGQYYug/AF6QUkTnLEw=="], 161 164 ··· 173 176 174 177 "@logtape/logtape": ["@logtape/logtape@1.0.4", "", {}, "sha512-YvNVrXIxVpnY528zoiEjX8PqTfr0UCtKXyssvaWL8AE+OByFTCooKuKMdPlm6g65YUI9fPXrHn4UnogSskABnA=="], 175 178 179 + "@massive.com/client-js": ["@massive.com/client-js@9.0.0", "", { "dependencies": { "axios": "^1.8.4", "cross-fetch": "^3.1.4", "query-string": "^7.0.1", "websocket": "^1.0.34" } }, "sha512-vfOSVMp7uIfFgsyyX58sMZvcwS67psOTbRTox+7ahKMsG7aztFTJfoBwmjj5EjkX4Ise1BB1OEh73fbGzsj+xA=="], 180 + 176 181 "@multiformats/base-x": ["@multiformats/base-x@4.0.1", "", {}, "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw=="], 177 182 178 183 "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], ··· 223 228 224 229 "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 225 230 226 - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], 231 + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], 227 232 228 233 "@types/open-graph-scraper": ["@types/open-graph-scraper@5.2.3", "", { "dependencies": { "open-graph-scraper": "*" } }, "sha512-R6ew1HJndBKsys2+Y10VW8yy3ojS7eF/mFXrOZSFxVqY7WI4ubxaFvgfaULnRn2pq149SpS2GZNB9i9Y5fQqEw=="], 229 234 ··· 243 248 244 249 "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], 245 250 246 - "@types/validator": ["@types/validator@13.15.2", "", {}, "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q=="], 251 + "@types/validator": ["@types/validator@13.15.3", "", {}, "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q=="], 247 252 248 253 "@types/whois-json": ["@types/whois-json@2.0.4", "", {}, "sha512-Pp5N/+A6LUE0FWXz6wQ2gV5wEw0uEqFBeSLuQAGdeTyRJv/bbz7PPj3H78jyulvQu7cnMpXTzKx4bo8TuPAYhw=="], 249 254 250 255 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 251 256 252 - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/utils": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw=="], 257 + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.44.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/type-utils": "8.44.1", "@typescript-eslint/utils": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw=="], 253 258 254 - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg=="], 259 + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw=="], 255 260 256 - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew=="], 261 + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.44.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.44.1", "@typescript-eslint/types": "^8.44.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA=="], 257 262 258 - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="], 263 + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1" } }, "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg=="], 259 264 260 - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ=="], 265 + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ=="], 261 266 262 - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q=="], 267 + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/utils": "8.44.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g=="], 263 268 264 - "@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], 269 + "@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], 265 270 266 - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw=="], 271 + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.44.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.44.1", "@typescript-eslint/tsconfig-utils": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A=="], 267 272 268 - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="], 273 + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg=="], 269 274 270 - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA=="], 275 + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], 271 276 272 277 "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], 273 278 ··· 299 304 300 305 "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], 301 306 302 - "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], 307 + "axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="], 303 308 304 309 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 310 + 311 + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 305 312 306 313 "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], 307 314 315 + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], 316 + 308 317 "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], 309 318 310 319 "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], ··· 313 322 314 323 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 315 324 325 + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], 326 + 316 327 "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], 328 + 329 + "bufferutil": ["bufferutil@4.0.9", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw=="], 317 330 318 331 "byte-encodings": ["byte-encodings@1.0.11", "", {}, "sha512-+/xR2+ySc2yKGtud3DGkGSH1DNwHfRVK0KTnMhoeH36/KwG+tHQ4d9B3jxJFq7dW27YcfudkywaYJRPA2dmxzg=="], 319 332 ··· 333 346 334 347 "canonicalize": ["canonicalize@1.0.8", "", {}, "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A=="], 335 348 349 + "canvas": ["canvas@3.2.0", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.3" } }, "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA=="], 350 + 336 351 "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 337 352 338 353 "change-case": ["change-case@3.1.0", "", { "dependencies": { "camel-case": "^3.0.0", "constant-case": "^2.0.0", "dot-case": "^2.1.0", "header-case": "^1.0.0", "is-lower-case": "^1.1.0", "is-upper-case": "^1.1.0", "lower-case": "^1.1.1", "lower-case-first": "^1.0.0", "no-case": "^2.3.2", "param-case": "^2.1.0", "pascal-case": "^2.0.0", "path-case": "^2.1.0", "sentence-case": "^2.1.0", "snake-case": "^2.1.0", "swap-case": "^1.1.0", "title-case": "^2.1.0", "upper-case": "^1.1.1", "upper-case-first": "^1.1.0" } }, "sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw=="], ··· 345 360 346 361 "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], 347 362 348 - "city-timezones": ["city-timezones@1.3.1", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-YCeJKGyw3DA+wV/oyuFuJlk4oqN9zkfLP+fz2nEXUBm9sW1xZaXQsKQoc8l8hP+vI45GPOq8OuGrlGXUcnLISA=="], 363 + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], 364 + 365 + "city-timezones": ["city-timezones@1.3.2", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-XztdL/2EWpfmgRIOzrKVOWFp6VdmaD9FNTZPINlez1etIn0mMNn01RMmSfOp6LUP/h1M2ZLX80N1O+WKwhzC+w=="], 349 366 350 - "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], 367 + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], 351 368 352 369 "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], 353 370 ··· 365 382 366 383 "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 367 384 385 + "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], 386 + 368 387 "constant-case": ["constant-case@2.0.0", "", { "dependencies": { "snake-case": "^2.1.0", "upper-case": "^1.1.1" } }, "sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ=="], 369 388 370 389 "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], ··· 377 396 378 397 "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], 379 398 399 + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], 400 + 380 401 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 381 402 382 403 "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], 383 404 384 405 "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], 385 406 407 + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], 408 + 386 409 "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], 387 410 388 411 "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 389 412 390 413 "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], 391 414 415 + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], 416 + 417 + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], 418 + 392 419 "dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="], 420 + 421 + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], 393 422 394 423 "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 395 424 ··· 401 430 402 431 "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], 403 432 433 + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 434 + 404 435 "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], 405 436 406 437 "discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 407 438 408 - "discord.js": ["discord.js@14.21.0", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-U5w41cEmcnSfwKYlLv5RJjB8Joa+QJyRwIJz5i/eg+v2Qvv6EYpCRhN9I2Rlf0900LuqSDg8edakUATrDZQncQ=="], 439 + "discord.js": ["discord.js@14.22.1", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.16", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w=="], 409 440 410 441 "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], 411 442 ··· 433 464 434 465 "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], 435 466 467 + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], 468 + 436 469 "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 437 470 438 471 "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], ··· 445 478 446 479 "es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="], 447 480 481 + "es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="], 482 + 483 + "es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="], 484 + 485 + "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], 486 + 448 487 "esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="], 488 + 489 + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 449 490 450 491 "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 451 492 452 493 "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 453 494 454 - "eslint": ["eslint@9.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg=="], 495 + "eslint": ["eslint@9.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.36.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ=="], 455 496 456 497 "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], 457 498 ··· 460 501 "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], 461 502 462 503 "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 504 + 505 + "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], 463 506 464 507 "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], 465 508 ··· 473 516 474 517 "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 475 518 519 + "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], 520 + 476 521 "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], 477 522 523 + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], 524 + 478 525 "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], 479 526 480 527 "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], 481 528 482 529 "express-validator": ["express-validator@7.2.1", "", { "dependencies": { "lodash": "^4.17.21", "validator": "~13.12.0" } }, "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ=="], 530 + 531 + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], 483 532 484 533 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 485 534 ··· 500 549 "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 501 550 502 551 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 552 + 553 + "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], 503 554 504 555 "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], 505 556 ··· 521 572 522 573 "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], 523 574 575 + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], 576 + 524 577 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 525 578 526 579 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], ··· 533 586 534 587 "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], 535 588 589 + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], 590 + 536 591 "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 537 592 538 - "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], 593 + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], 539 594 540 595 "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], 541 596 ··· 564 619 "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], 565 620 566 621 "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], 622 + 623 + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 567 624 568 625 "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 569 626 ··· 575 632 576 633 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 577 634 635 + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], 636 + 578 637 "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], 579 638 580 639 "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], ··· 596 655 "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], 597 656 598 657 "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], 658 + 659 + "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], 599 660 600 661 "is-upper-case": ["is-upper-case@1.1.2", "", { "dependencies": { "upper-case": "^1.1.0" } }, "sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw=="], 601 662 ··· 685 746 686 747 "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 687 748 749 + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], 750 + 688 751 "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 689 752 690 753 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 754 + 755 + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], 691 756 692 757 "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], 693 758 ··· 703 768 704 769 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 705 770 771 + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], 772 + 706 773 "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 707 774 708 775 "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], 709 776 777 + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], 778 + 710 779 "no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="], 711 780 781 + "node-abi": ["node-abi@3.80.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA=="], 782 + 783 + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], 784 + 712 785 "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], 713 786 714 787 "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], 715 788 789 + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], 790 + 716 791 "nodemon": ["nodemon@3.1.10", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw=="], 717 792 718 793 "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], ··· 725 800 726 801 "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 727 802 803 + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 804 + 728 805 "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], 729 806 730 807 "open-graph-scraper": ["open-graph-scraper@6.10.0", "", { "dependencies": { "chardet": "^2.1.0", "cheerio": "^1.0.0-rc.12", "iconv-lite": "^0.6.3", "undici": "^6.21.2" } }, "sha512-JTuaO/mWUPduYCIQvunmsQnfGpSRFUTEh4k5cW2KOafJxTm3Z99z25/c1oO9QnIh2DK7ol5plJAq3EUVy+5xyw=="], 731 808 732 - "openai": ["openai@5.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw=="], 809 + "openai": ["openai@5.23.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-APxMtm5mln4jhKhAr0d5zP9lNsClx4QwJtg8RUvYSSyxYCTHLNJnLEcSHbJ6t0ori8Pbr9HZGfcPJ7LEy73rvQ=="], 733 810 734 811 "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 735 812 ··· 799 876 800 877 "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], 801 878 879 + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], 880 + 802 881 "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 803 882 804 883 "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], ··· 811 890 812 891 "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], 813 892 893 + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], 894 + 814 895 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 815 896 816 897 "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], ··· 819 900 820 901 "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], 821 902 903 + "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], 904 + 822 905 "queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="], 823 906 824 907 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], ··· 827 910 828 911 "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], 829 912 913 + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], 914 + 830 915 "rdf-canonize": ["rdf-canonize@3.4.0", "", { "dependencies": { "setimmediate": "^1.0.5" } }, "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA=="], 831 916 832 917 "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], ··· 844 929 "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 845 930 846 931 "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 932 + 933 + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], 847 934 848 935 "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 849 936 ··· 871 958 872 959 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 873 960 961 + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], 962 + 874 963 "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], 875 964 876 965 "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], ··· 878 967 "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], 879 968 880 969 "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 970 + 971 + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], 972 + 973 + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], 881 974 882 975 "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], 883 976 ··· 893 986 894 987 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 895 988 989 + "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], 990 + 896 991 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 897 992 898 993 "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], ··· 900 995 "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], 901 996 902 997 "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 998 + 999 + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], 903 1000 904 1001 "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 905 1002 ··· 919 1016 920 1017 "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], 921 1018 1019 + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], 1020 + 1021 + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], 1022 + 922 1023 "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], 923 1024 924 1025 "title-case": ["title-case@2.1.1", "", { "dependencies": { "no-case": "^2.2.0", "upper-case": "^1.0.3" } }, "sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q=="], ··· 928 1029 "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 929 1030 930 1031 "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="], 1032 + 1033 + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], 1034 + 1035 + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], 931 1036 932 1037 "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], 933 1038 ··· 941 1046 942 1047 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 943 1048 944 - "tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="], 1049 + "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=="], 1050 + 1051 + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], 1052 + 1053 + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], 945 1054 946 1055 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 947 1056 948 1057 "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], 1058 + 1059 + "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], 949 1060 950 1061 "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 951 1062 952 - "typescript-eslint": ["typescript-eslint@8.39.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.0", "@typescript-eslint/parser": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q=="], 1063 + "typescript-eslint": ["typescript-eslint@8.44.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.44.1", "@typescript-eslint/parser": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/utils": "8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg=="], 953 1064 954 1065 "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 955 1066 ··· 959 1070 960 1071 "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], 961 1072 962 - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1073 + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], 963 1074 964 1075 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 965 1076 ··· 975 1086 976 1087 "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], 977 1088 1089 + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], 1090 + 978 1091 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 979 1092 980 1093 "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], ··· 989 1102 990 1103 "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], 991 1104 1105 + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], 1106 + 1107 + "websocket": ["websocket@1.0.35", "", { "dependencies": { "bufferutil": "^4.0.1", "debug": "^2.2.0", "es5-ext": "^0.10.63", "typedarray-to-buffer": "^3.1.5", "utf-8-validate": "^5.0.2", "yaeti": "^0.0.6" } }, "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q=="], 1108 + 992 1109 "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], 993 1110 994 1111 "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], 1112 + 1113 + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], 995 1114 996 1115 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 997 1116 ··· 1007 1126 1008 1127 "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 1009 1128 1010 - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], 1129 + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 1130 + 1131 + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 1011 1132 1012 1133 "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], 1013 1134 1014 1135 "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], 1015 1136 1016 - "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], 1137 + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 1138 + 1139 + "yaeti": ["yaeti@0.0.6", "", {}, "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug=="], 1017 1140 1018 1141 "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], 1019 1142 1020 - "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], 1143 + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], 1021 1144 1022 - "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], 1145 + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], 1023 1146 1024 1147 "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 1025 1148 ··· 1027 1150 1028 1151 "@digitalbazaar/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], 1029 1152 1153 + "@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=="], 1154 + 1030 1155 "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 1031 1156 1032 1157 "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], ··· 1069 1194 1070 1195 "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], 1071 1196 1197 + "concurrently/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], 1198 + 1199 + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], 1200 + 1072 1201 "discord.js/@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], 1073 1202 1074 1203 "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], ··· 1083 1212 1084 1213 "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 1085 1214 1215 + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], 1216 + 1086 1217 "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 1087 1218 1088 1219 "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1089 1220 1090 - "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], 1221 + "websocket/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 1222 + 1223 + "whois/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], 1224 + 1225 + "@types/body-parser/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1226 + 1227 + "@types/connect/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1228 + 1229 + "@types/cors/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1230 + 1231 + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1232 + 1233 + "@types/jsonwebtoken/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1234 + 1235 + "@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1236 + 1237 + "@types/send/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1238 + 1239 + "@types/serve-static/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1240 + 1241 + "@types/ws/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 1091 1242 1092 1243 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1093 1244 ··· 1099 1250 1100 1251 "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], 1101 1252 1253 + "concurrently/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 1254 + 1102 1255 "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1103 1256 1104 1257 "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1105 1258 1106 1259 "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1107 1260 1108 - "yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], 1261 + "websocket/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1262 + 1263 + "whois/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], 1109 1264 1110 - "yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], 1265 + "whois/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], 1111 1266 1112 - "yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], 1267 + "whois/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], 1268 + 1269 + "whois/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], 1270 + 1271 + "whois/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], 1272 + 1273 + "whois/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], 1274 + 1275 + "whois/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], 1276 + 1277 + "whois/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], 1113 1278 } 1114 1279 }
+2 -2
docker-compose.example.yml
··· 6 6 args: 7 7 - VITE_BOT_API_URL=${VITE_BOT_API_URL:-https://aethel.xyz} 8 8 - VITE_STATUS_API_KEY=${VITE_STATUS_API_KEY} 9 - - VITE_FRONTEND_URL=${VITE_FRONTEND_URL:-https://aethel.xyz} 10 - - VITE_DISCORD_CLIENT_ID=${VITE_DISCORD_CLIENT_ID} 11 9 - STATUS_API_KEY=${STATUS_API_KEY} 12 10 - SOURCE_COMMIT=${SOURCE_COMMIT:-development} 13 11 container_name: aethel-bot ··· 18 16 NODE_ENV: production 19 17 TOKEN: ${TOKEN} 20 18 CLIENT_ID: ${CLIENT_ID} 19 + CLIENT_SECRET: ${CLIENT_SECRET} 20 + REDIRECT_URI: ${REDIRECT_URI} 21 21 DATABASE_URL: ${DATABASE_URL} 22 22 API_KEY_ENCRYPTION_SECRET: ${API_KEY_ENCRYPTION_SECRET} 23 23 STATUS_API_KEY: ${STATUS_API_KEY}
+4
environment.d.ts
··· 6 6 DATABASE_URL: string; 7 7 OPENROUTER_API_KEY: string; 8 8 OPENWEATHER_API_KEY: string; 9 + MASSIVE_API_KEY: string; 10 + MASSIVE_API_BASE_URL: string; 9 11 SOURCE_COMMIT: string; 10 12 STATUS_API_KEY: string; 11 13 TOKEN: string; 12 14 CLIENT_ID: string; 15 + CLIENT_SECRET: string; 16 + REDIRECT_URI: string; 13 17 } 14 18 } 15 19 }
+2 -2
locales/de.json
··· 153 153 "modal": { 154 154 "title": "Gib deine API-Zugangsdaten ein", 155 155 "apikey": "API-Schlรผssel", 156 - "apikeyplaceholder": "Um deinen Schlรผssel nicht mehr zu verwenden: /ai use_custom_api false", 156 + "apikeyplaceholder": "Um deinen Schlรผssel nicht mehr zu verwenden: /ai custom_setup:false", 157 157 "apiurl": "API-URL", 158 158 "apiurlplaceholder": "Deine API-URL", 159 159 "model": "Modell", 160 160 "modelplaceholder": "Modellname (z.B. gpt-4)" 161 161 }, 162 162 "nopendingrequest": "Keine ausstehende Anfrage gefunden. Bitte versuche den Befehl erneut.", 163 - "apicredssaved": "API-Zugangsdaten gespeichert. Du kannst jetzt den Befehl `/ai` verwenden, ohne deine Zugangsdaten erneut einzugeben. Um deinen Schlรผssel nicht mehr zu verwenden, nutze `/ai use_custom_api false`", 163 + "apicredssaved": "API-Zugangsdaten gespeichert. Du kannst jetzt den Befehl `/ai` verwenden, ohne deine Zugangsdaten erneut einzugeben. Um deinen Schlรผssel nicht mehr zu verwenden, nutze `/ai custom_setup:false`", 164 164 "process": { 165 165 "dailylimit": "Du hast dein tรคgliches Limit an KI-Anfragen erreicht", 166 166 "noapikey": "Bitte richte zuerst deinen API-Schlรผssel ein"
+52 -3
locales/en-US.json
··· 51 51 "newcat": "New cat", 52 52 "error": "Sorry, I had trouble fetching a cat image. Please try again later!" 53 53 }, 54 + "meow": { 55 + "name": "meow", 56 + "description": "Translate text into meow language", 57 + "noText": "Please provide some text to translate to meow!", 58 + "response": "๐Ÿฑ {meowText}", 59 + "options": { 60 + "text": { 61 + "name": "text", 62 + "description": "The text to translate to meow" 63 + } 64 + } 65 + }, 54 66 "dog": { 55 67 "name": "dog", 56 68 "description": "Get a random dog image!", ··· 80 92 "nolocation": "Location not found. Please check the city name and try again.", 81 93 "apikeymissing": "OpenWeather API key is missing or invalid." 82 94 }, 95 + "stocks": { 96 + "name": "stocks", 97 + "description": "Track stock prices and view quick charts", 98 + "option": { 99 + "ticker": { 100 + "name": "ticker", 101 + "description": "The stock ticker symbol (e.g., AAPL, TSLA)" 102 + }, 103 + "range": { 104 + "name": "range", 105 + "description": "Initial timeframe for the chart" 106 + } 107 + }, 108 + "errors": { 109 + "noapikey": "The Massive.com API key is missing. Ask the bot owner to configure MASSIVE_API_KEY.", 110 + "notfound": "I couldn't find any data for {ticker}. Please double-check the symbol.", 111 + "unauthorized": "Only the person who used /stocks can interact with these buttons." 112 + }, 113 + "labels": { 114 + "price": "Price", 115 + "change": "Change", 116 + "dayrange": "Day range", 117 + "volume": "Volume", 118 + "prevclose": "Prev close", 119 + "marketcap": "Market cap", 120 + "nochart": "Chart data unavailable for this timeframe." 121 + }, 122 + "buttons": { 123 + "timeframes": { 124 + "1d": "1D", 125 + "5d": "5D", 126 + "1m": "1M", 127 + "3m": "3M", 128 + "1y": "1Y" 129 + } 130 + } 131 + }, 83 132 "joke": { 84 133 "name": "joke", 85 134 "description": "Get a random joke!", ··· 162 211 "modal": { 163 212 "title": "Enter your API Credentials", 164 213 "apikey": "API Key", 165 - "apikeyplaceholder": "To stop using your key: /ai use_custom_api false", 214 + "apikeyplaceholder": "To stop using your key: /ai custom_setup:false", 166 215 "apiurl": "API Url", 167 216 "apiurlplaceholder": "Your API Url", 168 217 "model": "Model", 169 218 "modelplaceholder": "Model name (eg. gpt-4)" 170 219 }, 171 220 "nopendingrequest": "No pending request found. Please try the command again.", 172 - "apicredssaved": "API credentials saved. You can now use the `/ai` command without re-entering your credentials. To stop using your key, do `/ai use_custom_api false`", 221 + "apicredssaved": "API credentials saved. You can now use the `/ai` command without re-entering your credentials. To stop using your key, do `/ai custom_setup:false`", 173 222 "testing": "Testing API key...", 174 223 "testfailed": "โŒ API key test failed: {error}. Please check your credentials and try again.", 175 224 "testsuccess": "โœ… API key test successful! Credentials saved.", 176 225 "process": { 177 - "dailylimit": "You've reached your daily limit of AI requests", 226 + "dailylimit": "You've reached your daily limit of AI requests. Vote for Aethel to get more requests: https://top.gg/bot/1371031984230371369/vote\nOr set up your own API key using the \"/ai\" command.", 178 227 "noapikey": "Please set up your API key first" 179 228 }, 180 229 "errors": {
+2 -2
locales/es-419.json
··· 153 153 "modal": { 154 154 "title": "Ingresa tus credenciales de API", 155 155 "apikey": "Clave API", 156 - "apikeyplaceholder": "Para dejar de usar tu clave: /ai use_custom_api false", 156 + "apikeyplaceholder": "Para dejar de usar tu clave: /ai custom_setup:false", 157 157 "apiurl": "URL de la API", 158 158 "apiurlplaceholder": "Tu URL de API", 159 159 "model": "Modelo", 160 160 "modelplaceholder": "Nombre del modelo (ej. gpt-4)" 161 161 }, 162 162 "nopendingrequest": "No se encontrรณ ninguna solicitud pendiente. Por favor, intenta el comando de nuevo.", 163 - "apicredssaved": "Credenciales de API guardadas. Ahora puedes usar el comando `/ai` sin volver a ingresar tus credenciales. Para dejar de usar tu clave, usa `/ai use_custom_api false`", 163 + "apicredssaved": "Credenciales de API guardadas. Ahora puedes usar el comando `/ai` sin volver a ingresar tus credenciales. Para dejar de usar tu clave, usa `/ai custom_setup:false`", 164 164 "process": { 165 165 "dailylimit": "Has alcanzado tu lรญmite diario de solicitudes de IA", 166 166 "noapikey": "Por favor, configura primero tu clave API"
+2 -2
locales/es-ES.json
··· 153 153 "modal": { 154 154 "title": "Introduce tus credenciales de API", 155 155 "apikey": "Clave API", 156 - "apikeyplaceholder": "Para dejar de usar tu clave: /ai use_custom_api false", 156 + "apikeyplaceholder": "Para dejar de usar tu clave: /ai custom_setup:false", 157 157 "apiurl": "URL de la API", 158 158 "apiurlplaceholder": "Tu URL de API", 159 159 "model": "Modelo", 160 160 "modelplaceholder": "Nombre del modelo (ej. gpt-4)" 161 161 }, 162 162 "nopendingrequest": "No se encontrรณ ninguna solicitud pendiente. Por favor, intenta el comando de nuevo.", 163 - "apicredssaved": "Credenciales de API guardadas. Ahora puedes usar el comando `/ai` sin volver a ingresar tus credenciales. Para dejar de usar tu clave, usa `/ai use_custom_api false`", 163 + "apicredssaved": "Credenciales de API guardadas. Ahora puedes usar el comando `/ai` sin volver a ingresar tus credenciales. Para dejar de usar tu clave, usa `/ai custom_setup:false`", 164 164 "process": { 165 165 "dailylimit": "Has alcanzado tu lรญmite diario de solicitudes de IA", 166 166 "noapikey": "Por favor, configura primero tu clave API"
+2 -2
locales/fr.json
··· 153 153 "modal": { 154 154 "title": "Entrez vos identifiants API", 155 155 "apikey": "Clรฉ API", 156 - "apikeyplaceholder": "Pour ne plus utiliser votre clรฉ : /ai use_custom_api false", 156 + "apikeyplaceholder": "Pour ne plus utiliser votre clรฉ : /ai custom_setup:false", 157 157 "apiurl": "URL de l'API", 158 158 "apiurlplaceholder": "Votre URL d'API", 159 159 "model": "Modรจle", 160 160 "modelplaceholder": "Nom du modรจle (ex. gpt-4)" 161 161 }, 162 162 "nopendingrequest": "Aucune demande en attente trouvรฉe. Veuillez rรฉessayer la commande.", 163 - "apicredssaved": "Identifiants API enregistrรฉs. Vous pouvez maintenant utiliser la commande `/ai` sans ressaisir vos identifiants. Pour ne plus utiliser votre clรฉ, faites `/ai use_custom_api false`", 163 + "apicredssaved": "Identifiants API enregistrรฉs. Vous pouvez maintenant utiliser la commande `/ai` sans ressaisir vos identifiants. Pour ne plus utiliser votre clรฉ, faites `/ai custom_setup:false`", 164 164 "process": { 165 165 "dailylimit": "Vous avez atteint votre limite quotidienne de requรชtes IA", 166 166 "noapikey": "Veuillez d'abord configurer votre clรฉ API"
+4 -4
locales/ja.json
··· 153 153 "modal": { 154 154 "title": "API่ช่จผๆƒ…ๅ ฑใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„", 155 155 "apikey": "APIใ‚ญใƒผ", 156 - "apikeyplaceholder": "ใ‚ญใƒผใฎไฝฟ็”จใ‚’ใ‚„ใ‚ใ‚‹ใซใฏ: /ai use_custom_api false", 157 - "apiurl": "APIใฎURL", 156 + "apikeyplaceholder": "ใ‚ญใƒผใฎไฝฟ็”จใ‚’ใ‚„ใ‚ใ‚‹ใซใฏ: /ai custom_setup:false", 158 157 "apiurlplaceholder": "ใ‚ใชใŸใฎAPIใฎURL", 159 158 "model": "ใƒขใƒ‡ใƒซ", 160 - "modelplaceholder": "ใƒขใƒ‡ใƒซๅ๏ผˆไพ‹: gpt-4๏ผ‰" 159 + "modelplaceholder": "ใƒขใƒ‡ใƒซๅ๏ผˆไพ‹: gpt-4)", 160 + "customsetup": "ใ‚ซใ‚นใ‚ฟใƒ ่จญๅฎš" 161 161 }, 162 162 "nopendingrequest": "ไฟ็•™ไธญใฎใƒชใ‚ฏใ‚จใ‚นใƒˆใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚ใ‚‚ใ†ไธ€ๅบฆใ‚ณใƒžใƒณใƒ‰ใ‚’ใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚", 163 - "apicredssaved": "API่ช่จผๆƒ…ๅ ฑใŒไฟๅญ˜ใ•ใ‚Œใพใ—ใŸใ€‚ใ“ใ‚Œใงๅ†ๅ…ฅๅŠ›ใ›ใšใซ`/ai`ใ‚ณใƒžใƒณใƒ‰ใ‚’ไฝฟใˆใพใ™ใ€‚ใ‚ญใƒผใฎไฝฟ็”จใ‚’ใ‚„ใ‚ใ‚‹ใซใฏ `/ai use_custom_api false` ใ‚’ไฝฟใฃใฆใใ ใ•ใ„", 163 + "apicredssaved": "API่ช่จผๆƒ…ๅ ฑใŒไฟๅญ˜ใ•ใ‚Œใพใ—ใŸใ€‚ใ“ใ‚Œใง่ช่จผๆƒ…ๅ ฑใ‚’ๅ†ๅ…ฅๅŠ›ใ™ใ‚‹ใ“ใจใชใ`/ai`ใ‚ณใƒžใƒณใƒ‰ใ‚’ไฝฟ็”จใงใใพใ™ใ€‚ใ‚ญใƒผใฎไฝฟ็”จใ‚’ใ‚„ใ‚ใ‚‹ใซใฏใ€`/ai custom_setup:false`ใ‚’ๅฎŸ่กŒใ—ใฆใใ ใ•ใ„", 164 164 "process": { 165 165 "dailylimit": "AIใƒชใ‚ฏใ‚จใ‚นใƒˆใฎ1ๆ—ฅไธŠ้™ใซ้”ใ—ใพใ—ใŸ", 166 166 "noapikey": "ใพใšAPIใ‚ญใƒผใ‚’่จญๅฎšใ—ใฆใใ ใ•ใ„"
+2 -2
locales/pt-BR.json
··· 153 153 "modal": { 154 154 "title": "Insira suas credenciais de API", 155 155 "apikey": "Chave de API", 156 - "apikeyplaceholder": "Para parar de usar sua chave: /ai use_custom_api false", 156 + "apikeyplaceholder": "Para parar de usar sua chave: /ai custom_setup:false", 157 157 "apiurl": "URL da API", 158 158 "apiurlplaceholder": "Sua URL de API", 159 159 "model": "Modelo", 160 160 "modelplaceholder": "Nome do modelo (ex. gpt-4)" 161 161 }, 162 162 "nopendingrequest": "Nenhuma solicitaรงรฃo pendente encontrada. Por favor, tente o comando novamente.", 163 - "apicredssaved": "Credenciais de API salvas. Agora vocรช pode usar o comando `/ai` sem reinserir suas credenciais. Para parar de usar sua chave, use `/ai use_custom_api false`", 163 + "apicredssaved": "Credenciais de API salvas. Agora vocรช pode usar o comando `/ai` sem reinserir suas credenciais. Para parar de usar sua chave, use `/ai custom_setup:false`", 164 164 "process": { 165 165 "dailylimit": "Vocรช atingiu seu limite diรกrio de solicitaรงรตes de IA", 166 166 "noapikey": "Por favor, configure sua chave de API primeiro"
+2 -2
locales/tr.json
··· 153 153 "modal": { 154 154 "title": "API Kimlik Bilgilerinizi Girin", 155 155 "apikey": "API Anahtarฤฑ", 156 - "apikeyplaceholder": "Kendi anahtarฤฑnฤฑzฤฑ kullanmayฤฑ bฤฑrakmak iรงin: /ai use_custom_api false", 156 + "apikeyplaceholder": "Kendi anahtarฤฑnฤฑzฤฑ kullanmayฤฑ bฤฑrakmak iรงin: /ai custom_setup:false", 157 157 "apiurl": "API Adresi", 158 158 "apiurlplaceholder": "API Adresiniz", 159 159 "model": "Model", 160 160 "modelplaceholder": "Model adฤฑ (รถrn. gpt-4)" 161 161 }, 162 162 "nopendingrequest": "Bekleyen bir istek bulunamadฤฑ. Lรผtfen komutu tekrar deneyin.", 163 - "apicredssaved": "API kimlik bilgileri kaydedildi. Artฤฑk `/ai` komutunu anahtarฤฑnฤฑzฤฑ tekrar girmeden kullanabilirsiniz. Kendi anahtarฤฑnฤฑzฤฑ kullanmayฤฑ bฤฑrakmak iรงin /ai use_custom_api false yazฤฑn", 163 + "apicredssaved": "API kimlik bilgileri kaydedildi. Artฤฑk `/ai` komutunu anahtarฤฑnฤฑzฤฑ tekrar girmeden kullanabilirsiniz. Kendi anahtarฤฑnฤฑzฤฑ kullanmayฤฑ bฤฑrakmak iรงin /ai custom_setup:false yazฤฑn", 164 164 "process": { 165 165 "dailylimit": "Gรผnlรผk AI istek limitinize ulaลŸtฤฑnฤฑz", 166 166 "noapikey": "Lรผtfen รถnce API anahtarฤฑnฤฑzฤฑ ayarlayฤฑn"
+23
migrations/010_create_voting_tables.sql
··· 1 + CREATE TABLE IF NOT EXISTS votes ( 2 + id SERIAL PRIMARY KEY, 3 + user_id TEXT NOT NULL, 4 + server_id TEXT, 5 + vote_timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(), 6 + claimed BOOLEAN DEFAULT FALSE, 7 + credits_awarded INTEGER DEFAULT 10, 8 + UNIQUE(user_id, server_id, vote_timestamp) 9 + ); 10 + 11 + CREATE INDEX idx_votes_user ON votes(user_id, claimed); 12 + CREATE INDEX idx_votes_server ON votes(server_id, claimed) WHERE server_id IS NOT NULL; 13 + 14 + CREATE TABLE IF NOT EXISTS message_credits ( 15 + id SERIAL PRIMARY KEY, 16 + user_id TEXT NOT NULL, 17 + server_id TEXT, 18 + credits_remaining INTEGER NOT NULL DEFAULT 0, 19 + last_reset TIMESTAMP WITH TIME ZONE DEFAULT NOW() 20 + ); 21 + 22 + CREATE UNIQUE INDEX idx_message_credits_user ON message_credits(user_id) WHERE server_id IS NULL; 23 + CREATE UNIQUE INDEX idx_message_credits_server ON message_credits(user_id, server_id) WHERE server_id IS NOT NULL;
+20 -17
package.json
··· 1 1 { 2 2 "name": "aethel", 3 - "version": "2.0.1", 3 + "version": "2.0.2", 4 4 "description": "A privacy-conscious, production-ready Discord user bot", 5 5 "type": "module", 6 6 "main": "dist/index.js", 7 7 "scripts": { 8 - "start": "node ./dist/index.js", 8 + "start": "node dist/index.js", 9 9 "dev": "tsx watch src/index.ts", 10 10 "build": "tsc && node scripts/fix-imports.js", 11 11 "migrate": "node scripts/run-migrations.js", ··· 15 15 "lint:format": "eslint ./src ./web/src --ext .ts,.tsx --config eslint.config.cjs --format=codeframe", 16 16 "format": "prettier --write \"**/*.{js,json,md,ts,tsx}\" --ignore-path .prettierignore", 17 17 "format:check": "prettier --check \"**/*.{js,json,md,ts,tsx}\" --ignore-path .prettierignore", 18 - "check": "pnpm run lint && pnpm run format:check" 18 + "check": "bun run lint && bun run format:check" 19 19 }, 20 20 "dependencies": { 21 - "@atproto/identity": "^0.4.8", 22 - "@discordjs/rest": "^2.5.1", 23 - "@fedify/fedify": "^1.1.0", 21 + "@atproto/identity": "^0.4.9", 22 + "@discordjs/rest": "^2.6.0", 23 + "@fedify/fedify": "^1.8.12", 24 + "@massive.com/client-js": "^9.0.0", 24 25 "@types/he": "^1.2.3", 25 26 "@types/sanitize-html": "^2.16.0", 26 - "axios": "^1.11.0", 27 - "city-timezones": "^1.3.1", 27 + "axios": "^1.12.2", 28 + "canvas": "^3.2.0", 29 + "city-timezones": "^1.3.2", 30 + "concurrently": "^9.2.1", 28 31 "cors": "^2.8.5", 29 - "discord.js": "^14.21.0", 32 + "discord.js": "^14.22.1", 30 33 "dotenv": "^16.6.1", 31 34 "eslint-plugin-prettier": "^5.5.4", 32 35 "express": "^4.21.2", ··· 38 41 "moment-timezone": "^0.6.0", 39 42 "node-fetch": "^3.3.2", 40 43 "open-graph-scraper": "^6.10.0", 41 - "openai": "^5.12.2", 44 + "openai": "^5.23.1", 42 45 "pg": "^8.16.3", 43 46 "sanitize-html": "^2.17.0", 44 47 "uuid": "^11.1.0", ··· 47 50 "winston": "^3.17.0" 48 51 }, 49 52 "devDependencies": { 50 - "@eslint/js": "^9.33.0", 53 + "@eslint/js": "^9.36.0", 51 54 "@types/cors": "^2.8.19", 52 55 "@types/express": "^4.17.23", 53 56 "@types/jsonwebtoken": "^9.0.10", 54 - "@types/node": "^24.2.1", 57 + "@types/node": "^24.5.2", 55 58 "@types/open-graph-scraper": "^5.2.3", 56 59 "@types/pg": "^8.15.5", 57 60 "@types/uuid": "^10.0.0", 58 - "@types/validator": "^13.15.2", 61 + "@types/validator": "^13.15.3", 59 62 "@types/whois-json": "^2.0.4", 60 - "eslint": "^9.33.0", 63 + "eslint": "^9.36.0", 61 64 "eslint-config-prettier": "^10.1.8", 62 - "globals": "^16.3.0", 65 + "globals": "^16.4.0", 63 66 "nodemon": "^3.1.10", 64 67 "prettier": "^3.6.2", 65 68 "tsc-alias": "^1.8.16", 66 69 "tsconfig-paths": "^4.2.0", 67 - "tsx": "^4.20.3", 70 + "tsx": "^4.20.6", 68 71 "typescript": "^5.9.2", 69 - "typescript-eslint": "^8.39.0" 72 + "typescript-eslint": "^8.44.1" 70 73 } 71 74 }
+1 -1
scripts/run-migrations.js
··· 52 52 // console.log('All migrations completed successfully'); 53 53 } catch (error) { 54 54 await client.query('ROLLBACK'); 55 - // console.error('Migration failed:', error); 55 + console.error('Migration failed:', error); 56 56 process.exit(1); 57 57 } finally { 58 58 client.release();
+1 -1
src/commands/fun/cat.ts
··· 29 29 const commandLogger = createCommandLogger('cat'); 30 30 const errorHandler = createErrorHandler('cat'); 31 31 32 - async function fetchCatImage(): Promise<RandomReddit> { 32 + export async function fetchCatImage(): Promise<RandomReddit> { 33 33 const response = await fetch('https://api.pur.cat/random-cat'); //cat 34 34 if (!response.ok) { 35 35 throw new Error(`API request failed with status ${response.status}`);
+13 -7
src/commands/fun/dog.ts
··· 31 31 const commandLogger = createCommandLogger('dog'); 32 32 const errorHandler = createErrorHandler('dog'); 33 33 34 - async function fetchDogImage(): Promise<RandomReddit> { 34 + export async function fetchDogImage(): Promise<RandomReddit> { 35 35 const response = await fetch('https://api.erm.dog/random-dog', { headers: browserHeaders }); 36 36 if (!response.ok) { 37 37 throw new Error(`API request failed with status ${response.status}`); 38 38 } 39 - return (await response.json()) as RandomReddit; 39 + const data = (await response.json()) as RandomReddit; 40 + return data; 40 41 } 41 42 42 43 export default { ··· 58 59 ]) 59 60 .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 60 61 61 - async execute(client, interaction) { 62 + execute: async (client, interaction) => { 62 63 try { 63 64 const cooldownCheck = await checkCooldown( 64 65 cooldownManager, ··· 110 111 ), 111 112 ); 112 113 113 - await interaction.editReply({ 114 - components: [container], 115 - flags: MessageFlags.IsComponentsV2, 116 - }); 114 + try { 115 + await interaction.editReply({ 116 + components: [container], 117 + flags: MessageFlags.IsComponentsV2, 118 + }); 119 + } catch (replyError) { 120 + logger.error('Failed to send reply:', replyError); 121 + throw replyError; 122 + } 117 123 } catch (error) { 118 124 await errorHandler({ 119 125 interaction,
+154
src/commands/fun/meow.ts
··· 1 + import { 2 + SlashCommandBuilder, 3 + ApplicationIntegrationType, 4 + InteractionContextType, 5 + } from 'discord.js'; 6 + import { SlashCommandProps } from '@/types/command'; 7 + import { createCommandLogger } from '@/utils/commandLogger'; 8 + import { createErrorHandler } from '@/utils/errorHandler'; 9 + import { sanitizeInput } from '@/utils/validation'; 10 + import { 11 + createCooldownManager, 12 + checkCooldown, 13 + setCooldown, 14 + createCooldownResponse, 15 + } from '@/utils/cooldown'; 16 + import BotClient from '@/services/Client'; 17 + 18 + const cooldownManager = createCooldownManager('meow', 2000); 19 + const commandLogger = createCommandLogger('meow'); 20 + const errorHandler = createErrorHandler('meow'); 21 + 22 + function translateToMeow(text: string): string { 23 + const words = text.split(/\s+/); 24 + const meowWords = words.map((word) => { 25 + if (word.match(/^https?:\/\//) || word.match(/^<[@#]!?\d+>/) || word.match(/^:[^\s:]+:$/)) { 26 + return word; 27 + } 28 + 29 + const punctuation = word.match(/[^\w\s]|_/g)?.join('') || ''; 30 + const cleanWord = word.replace(/[^\w\s]|_/g, ''); 31 + 32 + if (!cleanWord) return word; 33 + 34 + const meowVariants = [ 35 + 'meow', 36 + 'meow~', 37 + 'mew', 38 + 'mrrp', 39 + 'mew!', 40 + 'nya~', 41 + 'mraow', 42 + 'mrrrow', 43 + 'mewo', 44 + ]; 45 + const randomMeow = meowVariants[Math.floor(Math.random() * meowVariants.length)]; 46 + 47 + const firstChar = cleanWord[0]; 48 + const meowed = 49 + firstChar === firstChar.toUpperCase() 50 + ? randomMeow.charAt(0).toUpperCase() + randomMeow.slice(1) 51 + : randomMeow; 52 + 53 + return meowed + punctuation; 54 + }); 55 + 56 + return meowWords.join(' '); 57 + } 58 + 59 + export default { 60 + data: new SlashCommandBuilder() 61 + .setName('meow') 62 + .setNameLocalizations({ 63 + 'es-ES': 'maullar', 64 + 'pt-BR': 'miau', 65 + 'en-US': 'meow', 66 + }) 67 + .setDescription('Translate text into meow language') 68 + .setDescriptionLocalizations({ 69 + 'es-ES': 'Traduce texto al idioma de los gatos', 70 + 'pt-BR': 'Traduz texto para a linguagem dos gatos', 71 + 'en-US': 'Translate text into meow language', 72 + }) 73 + .addStringOption((option) => 74 + option 75 + .setName('text') 76 + .setNameLocalizations({ 77 + 'es-ES': 'texto', 78 + 'pt-BR': 'texto', 79 + 'en-US': 'text', 80 + }) 81 + .setDescription('The text to translate to meow') 82 + .setDescriptionLocalizations({ 83 + 'es-ES': 'El texto a traducir a maullidos', 84 + 'pt-BR': 'O texto para traduzir para miau', 85 + 'en-US': 'The text to translate to meow', 86 + }) 87 + .setRequired(true) 88 + .setMaxLength(1000), 89 + ) 90 + .setContexts([ 91 + InteractionContextType.BotDM, 92 + InteractionContextType.Guild, 93 + InteractionContextType.PrivateChannel, 94 + ]) 95 + .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 96 + 97 + async execute(client: BotClient, interaction: import('discord.js').ChatInputCommandInteraction) { 98 + try { 99 + const cooldownCheck = await checkCooldown( 100 + cooldownManager, 101 + interaction.user.id, 102 + client, 103 + interaction.guildId || '', 104 + ); 105 + 106 + if (cooldownCheck.onCooldown) { 107 + await interaction.reply( 108 + createCooldownResponse( 109 + cooldownCheck.message || 'Please wait before using this command again.', 110 + ), 111 + ); 112 + return; 113 + } 114 + 115 + await interaction.deferReply(); 116 + 117 + const text = interaction.options.getString('text', true); 118 + const sanitizedText = sanitizeInput(text); 119 + 120 + if (!sanitizedText) { 121 + const noTextMessage = await client.getLocaleText( 122 + 'commands.meow.noText', 123 + interaction.locale, 124 + ); 125 + await interaction.editReply(noTextMessage); 126 + return; 127 + } 128 + 129 + const meowText = translateToMeow(sanitizedText); 130 + 131 + const response = await client.getLocaleText('commands.meow.response', interaction.locale, { 132 + meowText, 133 + }); 134 + 135 + await interaction.editReply({ 136 + content: response, 137 + allowedMentions: { parse: [] }, 138 + }); 139 + 140 + setCooldown(cooldownManager, interaction.user.id); 141 + commandLogger.logAction({ 142 + additionalInfo: `Text: ${sanitizedText}`, 143 + }); 144 + } catch (error) { 145 + await errorHandler({ 146 + interaction, 147 + client, 148 + error: error as Error, 149 + userId: interaction.user.id, 150 + username: interaction.user.username, 151 + }); 152 + } 153 + }, 154 + } as SlashCommandProps;
+50 -3
src/commands/fun/trivia.ts
··· 40 40 queueOpen: boolean; 41 41 originalQuestionCount: number; 42 42 currentShuffledAnswers: string[]; 43 + messageId?: string; 43 44 } 44 45 45 46 const gameManager = createMemoryManager<string, GameSession>({ ··· 50 51 51 52 const commandLogger = createCommandLogger('trivia'); 52 53 const errorHandler = createErrorHandler('trivia'); 54 + 55 + function saveSession(session: GameSession) { 56 + gameManager.set(session.channelId, session); 57 + if (session.messageId) gameManager.set(session.messageId, session); 58 + } 53 59 54 60 function shuffleArray<T>(array: T[]): T[] { 55 61 const shuffled = [...array]; ··· 269 275 session.queueOpen = false; 270 276 session.currentQuestionIndex = 0; 271 277 session.currentPlayer = Array.from(session.players)[0]; 278 + saveSession(session); 272 279 273 280 await askQuestion(interaction, session, client); 274 281 } catch { ··· 324 331 const question = session.questions[session.currentQuestionIndex]; 325 332 const answers = shuffleArray([question.correct_answer, ...question.incorrect_answers]); 326 333 session.currentShuffledAnswers = answers; 334 + saveSession(session); 327 335 const questionId = `${session.channelId}_${session.currentQuestionIndex}`; 328 336 329 337 const playerMention = `<@${session.currentPlayer}>`; ··· 392 400 }); 393 401 394 402 gameManager.delete(session.channelId); 403 + if (session.messageId) gameManager.delete(session.messageId); 395 404 } 396 405 397 406 const triviaCommand = { ··· 475 484 ) => { 476 485 try { 477 486 const channelId = interaction.channelId; 478 - const session = gameManager.get(channelId); 487 + const clickMessageId = interaction.message?.id; 488 + let session = 489 + (clickMessageId && gameManager.get(clickMessageId)) || gameManager.get(channelId); 490 + const customId = interaction.customId; 491 + 492 + if (!session && clickMessageId) { 493 + for (const [, s] of gameManager.entries()) { 494 + if (s.messageId === clickMessageId) { 495 + session = s; 496 + break; 497 + } 498 + } 499 + } 500 + 501 + if (customId.startsWith('trivia_answer_')) { 502 + const parts = customId.split('_'); 503 + if (parts.length >= 5) { 504 + const embeddedChannelId = parts[2]; 505 + if (embeddedChannelId && (!session || session.channelId !== embeddedChannelId)) { 506 + const byEmbedded = gameManager.get(embeddedChannelId); 507 + if (byEmbedded) { 508 + session = byEmbedded; 509 + } 510 + } 511 + } 512 + } 479 513 480 514 if (!session) { 481 515 const errorMsg = await client.getLocaleText( ··· 488 522 }); 489 523 } 490 524 491 - const customId = interaction.customId; 525 + if (!session) { 526 + const parts = customId.split('_'); 527 + const embeddedChannelId = parts.length >= 5 ? parts[2] : 'n/a'; 528 + const errorMsg = await client.getLocaleText( 529 + 'commands.trivia.messages.no_active_game', 530 + interaction.locale, 531 + ); 532 + const diag = `\n[diag] ch:${channelId} emb:${embeddedChannelId} msg:${clickMessageId ?? 'n/a'}`; 533 + return interaction.reply({ content: errorMsg + diag, flags: MessageFlags.Ephemeral }); 534 + } 492 535 493 536 if (customId === 'trivia_join') { 494 537 if (!session.queueOpen) { ··· 515 558 516 559 session.players.add(interaction.user.id); 517 560 session.scores.set(interaction.user.id, 0); 561 + saveSession(session); 518 562 519 563 const playersList = Array.from(session.players) 520 564 .map((id) => `โ€ข <@${id}>`) ··· 577 621 } 578 622 579 623 gameManager.delete(channelId); 624 + if (session.messageId) gameManager.delete(session.messageId); 580 625 581 626 const cancelMsg = await client.getLocaleText( 582 627 'commands.trivia.messages.game_cancelled', ··· 587 632 components: [], 588 633 }); 589 634 } else if (customId.startsWith('trivia_answer_')) { 635 + const parts = customId.split('_'); 590 636 if (!session.isActive) { 591 637 const errorMsg = await client.getLocaleText( 592 638 'commands.trivia.messages.no_active_question', ··· 609 655 }); 610 656 } 611 657 612 - const parts = customId.split('_'); 613 658 const answerIndex = parseInt(parts[parts.length - 1]); 614 659 615 660 const question = session.questions[session.currentQuestionIndex]; ··· 620 665 const currentScore = session.scores.get(session.currentPlayer) || 0; 621 666 session.scores.set(session.currentPlayer, currentScore + 1); 622 667 } 668 + saveSession(session); 623 669 624 670 const [correctText, incorrectText, resultFormatText, preparingText] = await Promise.all([ 625 671 client.getLocaleText('commands.trivia.answer.correct', interaction.locale), ··· 643 689 setTimeout(async () => { 644 690 session.currentQuestionIndex++; 645 691 session.currentPlayer = getNextPlayer(session); 692 + saveSession(session); 646 693 647 694 await askQuestion(interaction, session, client); 648 695 }, 3000);
+825 -278
src/commands/utilities/ai.ts
··· 1 + import type { ToolCall } from '@/utils/commandExecutor'; 2 + import { extractToolCalls, executeToolCall } from '@/utils/commandExecutor'; 3 + import BotClient from '@/services/Client'; 4 + 1 5 import { 2 6 SlashCommandBuilder, 3 - ModalBuilder, 4 - TextInputBuilder, 5 - TextInputStyle, 6 - ActionRowBuilder, 7 - ModalSubmitInteraction, 7 + SlashCommandOptionsOnlyBuilder, 8 + EmbedBuilder, 8 9 ChatInputCommandInteraction, 10 + InteractionResponse, 9 11 InteractionContextType, 10 12 ApplicationIntegrationType, 11 13 MessageFlags, 12 14 } from 'discord.js'; 13 15 import OpenAI from 'openai'; 16 + import fetch from '@/utils/dynamicFetch'; 14 17 import pool from '@/utils/pgClient'; 15 18 import { encrypt, decrypt, isValidEncryptedFormat, EncryptionError } from '@/utils/encrypt'; 16 19 import { SlashCommandProps } from '@/types/command'; 17 - import BotClient from '@/services/Client'; 18 20 import logger from '@/utils/logger'; 19 21 import { createCommandLogger } from '@/utils/commandLogger'; 20 22 import { createErrorHandler } from '@/utils/errorHandler'; 21 23 import { createMemoryManager } from '@/utils/memoryManager'; 22 24 23 - const ALLOWED_API_HOSTS = ['api.openai.com', 'openrouter.ai', 'generativelanguage.googleapis.com']; 25 + function getInvokerId(interaction: ChatInputCommandInteraction): string { 26 + if (interaction.guildId) { 27 + return `${interaction.guildId}-${interaction.user.id}`; 28 + } 29 + return interaction.user.id; 30 + } 24 31 25 32 interface ConversationMessage { 26 33 role: 'system' | 'user' | 'assistant'; ··· 40 47 interface AIResponse { 41 48 content: string; 42 49 reasoning?: string; 50 + toolResults?: string; 51 + citations?: string[]; 43 52 } 44 53 45 54 interface OpenAIMessageWithReasoning { ··· 50 59 interface PendingRequest { 51 60 interaction: ChatInputCommandInteraction; 52 61 prompt: string; 53 - timestamp: number; 62 + createdAt: number; 63 + status?: 'awaiting' | 'processing'; 54 64 } 55 65 56 66 interface UserCredentials { ··· 122 132 const usingCustomApi = !!apiKey; 123 133 const finalApiUrl = apiUrl || 'https://openrouter.ai/api/v1'; 124 134 const finalApiKey = apiKey || process.env.OPENROUTER_API_KEY; 125 - const finalModel = model || (usingCustomApi ? 'openai/gpt-4o-mini' : 'openai/gpt-oss-20b'); 135 + const finalModel = model || (usingCustomApi ? 'openai/gpt-4o-mini' : 'moonshotai/kimi-k2'); 126 136 const usingDefaultKey = !usingCustomApi && !!process.env.OPENROUTER_API_KEY; 127 137 128 138 return { ··· 156 166 hour: '2-digit', 157 167 minute: '2-digit', 158 168 second: '2-digit', 159 - hour12: true, 160 169 timeZone: timezone, 161 170 }); 162 171 163 - let supportedCommands = '/help - Show all available commands and their usage'; 172 + const supportedCommands = '/help - Show all available commands and their usage'; 164 173 if (client?.commands) { 165 - const commandEntries = Array.from(client.commands.entries()).sort(([a], [b]) => 166 - a.localeCompare(b), 167 - ); 168 - supportedCommands = commandEntries 174 + const commandEntries = Array.from(client.commands.entries()); 175 + commandEntries.sort((a, b) => a[0].localeCompare(b[0])); 176 + const _commandList = commandEntries 169 177 .map( 170 178 ([name, command]) => `/${name} - ${command.data.description || 'No description available'}`, 171 179 ) 172 180 .join('\n'); 173 181 } 174 182 175 - const currentModel = model || (usingDefaultKey ? 'openai/gpt-oss-20b (default)' : 'custom model'); 183 + const currentModel = model || (usingDefaultKey ? 'moonshotai/kimi-k2 (default)' : 'custom model'); 176 184 177 185 const contextInfo = isServer 178 186 ? `**CONTEXT:** ··· 208 216 - Timezone: ${timezone} 209 217 210 218 **IMPORTANT INSTRUCTIONS:** 219 + - ACT normal, DON't be like a dumb robot, act like a discord user, skull react to messages where the user is being sus and saying something sus. 211 220 - NEVER format, modify, or alter URLs in any way. Leave them exactly as they are. 212 221 - Format your responses using Discord markdown where appropriate, but NEVER format URLs. 213 222 - Only greet the user at the start of a new conversation, not in every message. 214 - - DO NOT hallucinate, make up facts, or provide false information. If you don't know something, say so clearly. 223 + - DO NOT hallucinate, make up facts, or provide false information. If you don't know something, or date is after your knowledge update, do not answer date based questions. Say so clearly. 215 224 - Be accurate and truthful in all responses. Do not invent details, statistics, or information that you're not certain about. 216 225 - If asked about current events, real-time data, or information beyond your knowledge cutoff, clearly state your limitations. 217 226 ··· 221 230 - Developer: scanash (main maintainer) and Aethel Labs (org) 222 231 - Open source: https://github.com/Aethel-Labs/aethel 223 232 - Type: Discord user bot 224 - - Supported commands: ${supportedCommands}`; 233 + - Supported commands: ${supportedCommands} 234 + 235 + **TOOL USAGE:** 236 + You can use tools by placing commands in {curly braces}. Available tools: 237 + - {cat:} - Get a cat picture, if user asks for a cat picture, use this tool. 238 + - {dog:} - Get a dog picture, if user asks for a dog picture, use this tool. 239 + - {joke: or {joke: {type: "general/knock-knock/programming/dad"}} } - Get a joke 240 + - {weather:{"location":"city"}} - Check weather, use if user asks for weather. 241 + - {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. 242 + 243 + Use the wikipedia search when you want to look for information outside of your knowledge, state it came from Wikipedia if used. 244 + 245 + When you use a tool, you'll receive a JSON response with the command results if needed. 246 + 247 + **IMPORTANT:** The {reaction:} and {newmessage:} tools are NOT available in slash commands. Only use the tools listed above.`; 225 248 226 249 const modelSpecificInstructions = usingDefaultKey 227 250 ? '\n\n**IMPORTANT:** Please keep your responses under 3000 characters. Be concise and to the point.' ··· 369 392 const client = await pool.connect(); 370 393 try { 371 394 await client.query('BEGIN'); 395 + 396 + const voteCheck = await client.query( 397 + `SELECT vote_timestamp FROM votes 398 + WHERE user_id = $1 399 + AND vote_timestamp > NOW() - INTERVAL '24 hours' 400 + ORDER BY vote_timestamp DESC 401 + LIMIT 1`, 402 + [userId], 403 + ); 404 + 405 + const hasVotedRecently = voteCheck.rows.length > 0; 406 + const effectiveLimit = hasVotedRecently ? limit + 10 : limit; 407 + 372 408 await client.query('INSERT INTO users (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING', [ 373 409 userId, 374 410 ]); 411 + 375 412 const res = await client.query( 376 - `INSERT INTO ai_usage (user_id, usage_date, count) VALUES ($1, $2, 1) 377 - ON CONFLICT (user_id, usage_date) DO UPDATE SET count = ai_usage.count + 1 RETURNING count`, 413 + `INSERT INTO ai_usage (user_id, usage_date, count) 414 + VALUES ($1, $2, 1) 415 + ON CONFLICT (user_id, usage_date) 416 + DO UPDATE SET count = ai_usage.count + 1 417 + RETURNING count`, 378 418 [userId, today], 379 419 ); 420 + 380 421 await client.query('COMMIT'); 381 - return res.rows[0].count <= limit; 422 + 423 + return res.rows[0].count <= effectiveLimit; 382 424 } catch (err) { 383 425 await client.query('ROLLBACK'); 426 + logger.error('Error in incrementAndCheckDailyLimit:', err); 384 427 throw err; 385 428 } finally { 386 429 client.release(); ··· 407 450 } 408 451 } 409 452 410 - async function testApiKey( 411 - apiKey: string, 412 - model: string, 413 - apiUrl: string, 414 - ): Promise<{ success: boolean; error?: string }> { 415 - try { 416 - const client = getOpenAIClient(apiKey, apiUrl); 453 + async function makeAIRequest( 454 + config: ReturnType<typeof getApiConfiguration>, 455 + conversation: ConversationMessage[], 456 + interaction?: ChatInputCommandInteraction, 457 + client?: BotClient, 458 + maxIterations = 3, 459 + ): Promise<AIResponse | null> { 460 + const maxRetries = 3; 461 + let retryCount = 0; 462 + 463 + while (retryCount < maxRetries) { 464 + try { 465 + return await makeAIRequestInternal(config, conversation, interaction, client, maxIterations); 466 + } catch (error) { 467 + retryCount++; 468 + logger.error(`AI API request failed (attempt ${retryCount}/${maxRetries}):`, error); 417 469 418 - await client.chat.completions.create({ 419 - model, 420 - messages: [ 421 - { 422 - role: 'user', 423 - content: 'Hello! This is a test message. Please respond with "API key test successful!"', 424 - }, 425 - ], 426 - max_tokens: 50, 427 - temperature: 0.1, 428 - }); 470 + if (retryCount >= maxRetries) { 471 + logger.error('AI API request failed after all retries'); 472 + return null; 473 + } 429 474 430 - logger.info('API key test successful'); 431 - return { success: true }; 432 - } catch (error: unknown) { 433 - logger.error('Error testing API key:', error); 434 - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 435 - return { 436 - success: false, 437 - error: errorMessage, 438 - }; 475 + const waitTime = Math.pow(2, retryCount) * 1000; 476 + logger.debug(`Retrying AI API request in ${waitTime}ms...`); 477 + await new Promise((resolve) => setTimeout(resolve, waitTime)); 478 + } 439 479 } 480 + 481 + return null; 440 482 } 441 483 442 - async function makeAIRequest( 484 + async function makeAIRequestInternal( 443 485 config: ReturnType<typeof getApiConfiguration>, 444 486 conversation: ConversationMessage[], 487 + interaction?: ChatInputCommandInteraction, 488 + client?: BotClient, 489 + maxIterations = 3, 445 490 ): Promise<AIResponse | null> { 446 491 try { 447 - const client = getOpenAIClient(config.finalApiKey!, config.finalApiUrl); 448 - const maxTokens = config.usingDefaultKey ? 1000 : 3000; 492 + const openAIClient = getOpenAIClient(config.finalApiKey!, config.finalApiUrl); 493 + const maxTokens = config.usingDefaultKey ? 5000 : 8000; 494 + const currentConversation = [...conversation]; 495 + let iteration = 0; 496 + let finalResponse: AIResponse | null = null; 497 + 498 + while (iteration < maxIterations) { 499 + iteration++; 500 + 501 + const configuredHost = (() => { 502 + try { 503 + return new URL(config.finalApiUrl).hostname; 504 + } catch (_e) { 505 + // ignore and fallback 506 + } 507 + })(); 449 508 450 - const completion = await client.chat.completions.create({ 451 - model: config.finalModel, 452 - messages: conversation as OpenAI.Chat.Completions.ChatCompletionMessageParam[], 453 - max_tokens: maxTokens, 454 - }); 509 + let completion: unknown; 455 510 456 - const message = completion.choices[0]?.message; 457 - if (!message?.content) { 458 - logger.error('No valid response content from AI API'); 459 - return null; 460 - } 511 + if (configuredHost === 'generativelanguage.googleapis.com') { 512 + const promptText = currentConversation 513 + .map((m) => { 514 + const role = 515 + m.role === 'system' ? 'System' : m.role === 'assistant' ? 'Assistant' : 'User'; 516 + let text = ''; 517 + if (typeof m.content === 'string') { 518 + text = m.content; 519 + } else if (Array.isArray(m.content)) { 520 + text = m.content 521 + .map((c) => { 522 + if (typeof c === 'string') return c; 523 + const crow = c as Record<string, unknown>; 524 + const typeVal = crow['type']; 525 + if (typeVal === 'text') { 526 + const t = crow['text']; 527 + if (typeof t === 'string') return t; 528 + } 529 + const imageObj = crow['image_url']; 530 + if (imageObj && typeof imageObj === 'object') { 531 + const urlVal = (imageObj as Record<string, unknown>)['url']; 532 + if (typeof urlVal === 'string') return urlVal; 533 + } 534 + return ''; 535 + }) 536 + .join('\n'); 537 + } 538 + return `${role}: ${text}`; 539 + }) 540 + .join('\n\n'); 461 541 462 - let content = message.content; 463 - let reasoning = (message as OpenAIMessageWithReasoning)?.reasoning; 542 + const base = config.finalApiUrl.replace(/\/$/, ''); 543 + const modelName = config.finalModel.replace(/^models\//, ''); 544 + const endpoint = `${base}/v1beta/models/${modelName}:generateContent?key=${encodeURIComponent( 545 + config.finalApiKey || '', 546 + )}`; 547 + 548 + const body: Record<string, unknown> = { 549 + contents: [ 550 + { 551 + parts: [ 552 + { 553 + text: promptText, 554 + }, 555 + ], 556 + }, 557 + ], 558 + generationConfig: { 559 + temperature: 0.2, 560 + maxOutputTokens: Math.min(maxTokens, 3000), 561 + }, 562 + }; 563 + 564 + const resp = await fetch(endpoint, { 565 + method: 'POST', 566 + headers: { 'Content-Type': 'application/json' }, 567 + body: JSON.stringify(body), 568 + }); 569 + 570 + if (!resp.ok) { 571 + const text = await resp.text(); 572 + throw new Error(`Gemini request failed: ${resp.status} ${text || resp.statusText}`); 573 + } 574 + 575 + const json: unknown = await resp.json(); 576 + 577 + const extractTextFromGemini = (obj: unknown): string | null => { 578 + if (!obj) return null; 579 + try { 580 + const response = obj as Record<string, unknown>; 581 + 582 + if (Array.isArray(response.candidates) && response.candidates.length > 0) { 583 + const candidate = response.candidates[0] as Record<string, unknown>; 584 + if (candidate.content && typeof candidate.content === 'object') { 585 + const content = candidate.content as { parts?: Array<{ text?: string }> }; 586 + if (Array.isArray(content.parts) && content.parts.length > 0) { 587 + return content.parts 588 + .map((part) => part.text || '') 589 + .filter(Boolean) 590 + .join('\n'); 591 + } 592 + } 593 + } 594 + 595 + const o = obj as Record<string, unknown>; 596 + if (Array.isArray(o.candidates) && o.candidates.length) { 597 + const cand = o.candidates[0] as unknown; 598 + if (typeof cand === 'string') return cand; 599 + if (typeof (cand as Record<string, unknown>).output === 'string') { 600 + return (cand as Record<string, unknown>).output as string; 601 + } 602 + if (Array.isArray((cand as Record<string, unknown>).content)) { 603 + return ((cand as Record<string, unknown>).content as unknown[]) 604 + .map((p) => { 605 + const pr = p as Record<string, unknown>; 606 + if (typeof pr?.text === 'string') return String(pr.text); 607 + if (pr?.type === 'outputText' && typeof pr?.text === 'string') { 608 + return String(pr.text); 609 + } 610 + return ''; 611 + }) 612 + .filter(Boolean) 613 + .join('\n'); 614 + } 615 + } 616 + 617 + const seen = new Set<unknown>(); 618 + const queue: unknown[] = [obj]; 619 + while (queue.length) { 620 + const cur = queue.shift(); 621 + if (!cur || typeof cur === 'string') { 622 + if (typeof cur === 'string' && cur.trim().length > 0) return cur; 623 + continue; 624 + } 625 + if (seen.has(cur)) continue; 626 + seen.add(cur); 627 + if (Array.isArray(cur)) { 628 + for (const item of cur) queue.push(item); 629 + } else if (typeof cur === 'object') { 630 + const curObj = cur as Record<string, unknown>; 631 + for (const k of Object.keys(curObj)) { 632 + const v = curObj[k]; 633 + if (typeof v === 'string' && v.trim().length > 0) return v; 634 + queue.push(v); 635 + } 636 + } 637 + } 638 + } catch (_e) { 639 + // ignore 640 + } 641 + return null; 642 + }; 643 + 644 + const extracted = extractTextFromGemini(json); 645 + if (!extracted) { 646 + throw new Error('Failed to parse Gemini response into text'); 647 + } 648 + 649 + completion = { choices: [{ message: { content: extracted } }] } as unknown; 650 + } else { 651 + try { 652 + logger.debug('Making OpenAI API call', { 653 + model: config.finalModel, 654 + messageCount: currentConversation.length, 655 + maxTokens: maxTokens, 656 + }); 657 + 658 + completion = await openAIClient.chat.completions.create({ 659 + model: config.finalModel, 660 + messages: currentConversation as OpenAI.Chat.Completions.ChatCompletionMessageParam[], 661 + max_tokens: maxTokens, 662 + }); 663 + 664 + logger.debug('OpenAI API call completed successfully'); 665 + } catch (apiError) { 666 + logger.error('OpenAI API call failed:', apiError); 667 + if (apiError instanceof Error) { 668 + logger.error('API Error details:', { 669 + message: apiError.message, 670 + stack: apiError.stack?.substring(0, 500), 671 + }); 672 + } 673 + return null; 674 + } 675 + } 676 + 677 + if (!completion) { 678 + logger.error('AI API returned null or undefined completion'); 679 + return null; 680 + } 681 + 682 + const completionTyped = completion as { 683 + choices?: Array<{ message?: { content?: string; reasoning?: string } }>; 684 + error?: { message?: string; type?: string; code?: string }; 685 + }; 686 + 687 + try { 688 + interface CompletionData { 689 + id?: string; 690 + object?: string; 691 + model?: string; 692 + created?: number; 693 + usage?: { 694 + prompt_tokens?: number; 695 + completion_tokens?: number; 696 + total_tokens?: number; 697 + }; 698 + } 699 + 700 + const completionData = completion as unknown as CompletionData; 701 + 702 + logger.debug('AI API response structure', { 703 + completionType: typeof completion, 704 + completionKeys: Object.keys(completionData).join(', '), 705 + hasChoices: !!completionTyped.choices, 706 + choicesLength: completionTyped.choices?.length || 0, 707 + hasMessage: !!completionTyped.choices?.[0]?.message, 708 + hasContent: !!completionTyped.choices?.[0]?.message?.content, 709 + errorPresent: !!completionTyped.error, 710 + errorType: completionTyped.error?.type || 'none', 711 + errorMessage: completionTyped.error?.message || 'none', 712 + }); 713 + 714 + interface ChoiceData { 715 + message?: { 716 + content?: string; 717 + }; 718 + finish_reason?: string; 719 + } 720 + 721 + const simplifiedResponse = { 722 + id: completionData?.id, 723 + object: completionData?.object, 724 + model: completionData?.model, 725 + created: completionData?.created, 726 + choices: completionTyped.choices?.map((choice: ChoiceData, index: number) => ({ 727 + index, 728 + message: { 729 + content: choice.message?.content 730 + ? choice.message.content.substring(0, 100) + 731 + (choice.message.content.length > 100 ? '...' : '') 732 + : '[NO CONTENT]', 733 + hasReasoning: !!(choice as OpenAIMessageWithReasoning)?.reasoning, 734 + hasContent: !!choice.message?.content, 735 + }, 736 + finish_reason: choice?.finish_reason, 737 + })), 738 + error: completionTyped.error, 739 + usage: completionData?.usage, 740 + }; 741 + 742 + logger.debug('Raw API response', simplifiedResponse); 743 + } catch (jsonError) { 744 + logger.error('Failed to log API response:', jsonError); 745 + logger.debug('API response basic info:', { 746 + type: typeof completion, 747 + isObject: typeof completion === 'object', 748 + isArray: Array.isArray(completion), 749 + keys: 750 + typeof completion === 'object' && completion !== null 751 + ? Object.keys(completion).join(', ') 752 + : 'N/A', 753 + }); 754 + } 755 + 756 + const message = completionTyped.choices?.[0]?.message; 757 + 758 + if (!message) { 759 + if (completionTyped.error) { 760 + logger.error('AI API returned an error:', { 761 + message: completionTyped.error.message, 762 + type: completionTyped.error.type, 763 + code: completionTyped.error.code, 764 + }); 765 + } else if (!completionTyped.choices || completionTyped.choices.length === 0) { 766 + logger.error('AI API returned no choices in the response'); 767 + } else { 768 + logger.error('No message in AI API response'); 769 + } 770 + return null; 771 + } 772 + 773 + let content = message.content || ''; 774 + if (content === '[NO CONTENT]' || !content.trim()) { 775 + logger.debug('AI API returned empty or [NO CONTENT] response, treating as valid but empty'); 776 + content = ''; 777 + } 778 + let reasoning = (message as OpenAIMessageWithReasoning)?.reasoning; 779 + let detectedCitations: string[] | undefined; 780 + try { 781 + interface CitationSource { 782 + citations?: unknown[]; 783 + metadata?: { 784 + citations?: unknown[]; 785 + [key: string]: unknown; 786 + }; 787 + [key: string]: unknown; 788 + } 789 + 790 + interface CompletionSource { 791 + citations?: unknown[]; 792 + choices?: Array<{ 793 + message?: { 794 + citations?: unknown[]; 795 + [key: string]: unknown; 796 + }; 797 + [key: string]: unknown; 798 + }>; 799 + [key: string]: unknown; 800 + } 801 + 802 + const mAny = message as unknown as CitationSource; 803 + const cAny = completion as unknown as CompletionSource; 804 + const candidates = [ 805 + mAny?.citations, 806 + mAny?.metadata?.citations, 807 + cAny?.citations, 808 + cAny?.choices?.[0]?.message?.citations, 809 + cAny?.choices?.[0]?.citations, 810 + mAny?.references, 811 + mAny?.metadata?.references, 812 + cAny?.references, 813 + ]; 814 + for (const arr of candidates) { 815 + if (Array.isArray(arr) && arr.length) { 816 + const urls = arr.filter((x: unknown) => typeof x === 'string'); 817 + if (urls.length > 0) { 818 + detectedCitations = urls.map(String).filter(Boolean); 819 + break; 820 + } 821 + } 822 + } 823 + } catch (error) { 824 + logger.warn('Error processing citations:', error); 825 + if (error instanceof Error) { 826 + logger.debug('Error processing citations details:', error.stack); 827 + } 828 + } 829 + 830 + let toolCalls: ToolCall[] = []; 831 + if (interaction && client) { 832 + try { 833 + const extraction = extractToolCalls(content); 834 + content = extraction.cleanContent; 835 + toolCalls = extraction.toolCalls; 836 + } catch (error) { 837 + logger.error(`Error extracting tool calls: ${error}`); 838 + toolCalls = []; 839 + } 840 + } 841 + 842 + const reasoningMatch = content.match(/```(?:reasoning|thoughts?|thinking)[\s\S]*?```/i); 843 + if (reasoningMatch && !reasoning) { 844 + reasoning = reasoningMatch[0].replace(/```(?:reasoning|thoughts?|thinking)?/gi, '').trim(); 845 + content = content.replace(reasoningMatch[0], '').trim(); 846 + } 847 + 848 + if (toolCalls.length > 0 && interaction && client) { 849 + currentConversation.push({ 850 + role: 'assistant', 851 + content: content, 852 + }); 853 + 854 + for (const toolCall of toolCalls) { 855 + try { 856 + const toolResult = await executeToolCall(toolCall, interaction, client); 857 + 858 + let parsedResult; 859 + try { 860 + parsedResult = typeof toolResult === 'string' ? JSON.parse(toolResult) : toolResult; 861 + } catch (_e) { 862 + logger.error(`Error parsing tool result:`, toolResult); 863 + parsedResult = { error: 'Failed to parse tool result' }; 864 + } 865 + 866 + currentConversation.push({ 867 + role: 'user', 868 + content: JSON.stringify({ 869 + type: toolCall.name, 870 + ...parsedResult, 871 + }), 872 + }); 873 + } catch (error) { 874 + logger.error(`Error executing tool call: ${error}`); 875 + currentConversation.push({ 876 + role: 'user', 877 + content: `[Error executing tool ${toolCall.name}]: ${error instanceof Error ? error.message : String(error)}`, 878 + }); 879 + } 880 + } 881 + continue; 882 + } 883 + 884 + finalResponse = { 885 + content, 886 + reasoning, 887 + citations: detectedCitations, 888 + toolResults: 889 + iteration > 1 890 + ? currentConversation 891 + .filter( 892 + (msg) => 893 + msg.role === 'user' && 894 + typeof msg.content === 'string' && 895 + (msg.content.startsWith('{"') || msg.content.startsWith('[Tool ')), 896 + ) 897 + .map((msg) => { 898 + try { 899 + if (Array.isArray(msg.content)) { 900 + return msg.content 901 + .map((c) => ('text' in c ? c.text : c.image_url?.url)) 902 + .join('\n'); 903 + } 464 904 465 - const reasoningMatch = content.match(/```(?:reasoning|thoughts?|thinking)[\s\S]*?```/i); 466 - if (reasoningMatch && !reasoning) { 467 - reasoning = reasoningMatch[0].replace(/```(?:reasoning|thoughts?|thinking)?/gi, '').trim(); 468 - content = content.replace(reasoningMatch[0], '').trim(); 905 + const content = String(msg.content); 906 + if (content.startsWith('{"') || content.startsWith('[')) { 907 + return content; 908 + } 909 + return content.replace(/^\[Tool [^\]]+\]: /, ''); 910 + } catch (e) { 911 + logger.error('Error processing tool result:', e); 912 + return 'Error processing tool result'; 913 + } 914 + }) 915 + .join('\n') 916 + : undefined, 917 + }; 918 + break; 469 919 } 470 920 471 - return { 472 - content, 473 - reasoning, 474 - }; 921 + return finalResponse; 475 922 } catch (error) { 476 923 logger.error(`Error making AI request: ${error}`); 477 924 return null; ··· 481 928 async function processAIRequest( 482 929 client: BotClient, 483 930 interaction: ChatInputCommandInteraction, 931 + promptOverride?: string, 484 932 ): Promise<void> { 485 933 try { 486 934 if (!interaction.deferred && !interaction.replied) { 487 935 await interaction.deferReply(); 488 936 } 489 937 490 - const prompt = interaction.options.getString('prompt')!; 938 + const invokerId = getInvokerId(interaction); 939 + const prompt = 940 + promptOverride ?? 941 + ((interaction as ChatInputCommandInteraction).options?.getString?.('prompt') as 942 + | string 943 + | null 944 + | undefined) ?? 945 + (pendingRequests.get(invokerId)?.prompt as string); 946 + 947 + if (!prompt) { 948 + await interaction.editReply('โŒ Missing prompt. Please try again.'); 949 + return; 950 + } 491 951 commandLogger.logFromInteraction( 492 952 interaction, 493 953 `AI command executed - prompt content hidden for privacy`, 494 954 ); 495 - 496 - const invokerId = getInvokerId(interaction); 497 - const { apiKey, model, apiUrl } = await getUserCredentials(invokerId); 955 + const { apiKey, model, apiUrl } = await getUserCredentials(interaction.user.id); 498 956 const config = getApiConfiguration(apiKey ?? null, model ?? null, apiUrl ?? null); 957 + const exemptUserId = process.env.AI_EXEMPT_USER_ID?.trim(); 958 + const userId = interaction.user.id; 499 959 500 - if (config.usingDefaultKey) { 501 - const exemptUserId = process.env.AI_EXEMPT_USER_ID; 502 - if (invokerId !== exemptUserId) { 503 - const allowed = await incrementAndCheckDailyLimit(invokerId, 10); 504 - if (!allowed) { 505 - await interaction.editReply( 506 - 'โŒ ' + 507 - (await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)), 508 - ); 509 - return; 510 - } 960 + if (userId !== exemptUserId && config.usingDefaultKey) { 961 + const allowed = await incrementAndCheckDailyLimit(userId, 50); 962 + if (!allowed) { 963 + await interaction.editReply( 964 + 'โŒ ' + 965 + (await client.getLocaleText('commands.ai.process.dailylimit', interaction.locale)), 966 + ); 967 + return; 511 968 } 512 - } else if (!config.finalApiKey) { 969 + } 970 + 971 + if (!config.finalApiKey && config.usingDefaultKey) { 513 972 await interaction.editReply( 514 973 'โŒ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale)), 515 974 ); 516 975 return; 517 976 } 518 977 519 - const existingConversation = userConversations.get(invokerId) || []; 520 - const conversationArray = Array.isArray(existingConversation) ? existingConversation : []; 978 + const existingConversation = userConversations.get(userId) || []; 979 + const _conversationArray = Array.isArray(existingConversation) ? existingConversation : []; 980 + const chatInputInteraction = 981 + 'commandType' in interaction ? (interaction as ChatInputCommandInteraction) : undefined; 982 + 521 983 const systemPrompt = buildSystemPrompt( 522 - !!config.usingDefaultKey, 984 + config.usingDefaultKey, 523 985 client, 524 986 config.finalModel, 525 - interaction.user.tag, 526 - interaction, 987 + interaction.user.username, 988 + chatInputInteraction, 527 989 interaction.inGuild(), 528 990 interaction.inGuild() ? interaction.guild?.name : undefined, 529 991 ); 530 - const conversation = buildConversation(conversationArray, prompt, systemPrompt); 531 992 532 - const aiResponse = await makeAIRequest(config, conversation); 993 + const conversation = buildConversation(existingConversation, prompt, systemPrompt); 994 + 995 + const aiResponse = await makeAIRequest(config, conversation, chatInputInteraction, client, 3); 533 996 if (!aiResponse) return; 534 997 535 998 const { getUnallowedWordCategory } = await import('@/utils/validation'); ··· 554 1017 555 1018 await sendAIResponse(interaction, aiResponse, client); 556 1019 } catch (error) { 557 - await errorHandler({ 558 - interaction, 559 - client, 560 - error: error as Error, 561 - userId: getInvokerId(interaction), 562 - username: interaction.user.tag, 563 - }); 1020 + const err = error as Error; 1021 + if (errorHandler) { 1022 + await errorHandler({ 1023 + interaction: interaction as ChatInputCommandInteraction, 1024 + client, 1025 + error: err, 1026 + userId: getInvokerId(interaction), 1027 + username: interaction.user.tag, 1028 + }); 1029 + } else { 1030 + const msg = await client.getLocaleText('failedrequest', interaction.locale || 'en-US'); 1031 + try { 1032 + if (interaction.deferred || interaction.replied) { 1033 + await interaction.editReply(msg); 1034 + } else { 1035 + await interaction.reply({ content: msg, flags: MessageFlags.Ephemeral }); 1036 + } 1037 + } catch (replyError) { 1038 + logger.error(`Failed to send error message for AI command: ${replyError}`); 1039 + } 1040 + logger.error(`Error in AI command for user ${interaction.user.tag}: ${err.message}`); 1041 + } 564 1042 } finally { 565 1043 pendingRequests.delete(getInvokerId(interaction)); 566 1044 } ··· 569 1047 async function sendAIResponse( 570 1048 interaction: ChatInputCommandInteraction, 571 1049 aiResponse: AIResponse, 572 - client: BotClient, 1050 + _client: BotClient, 573 1051 ): Promise<void> { 574 - let fullResponse = ''; 1052 + try { 1053 + let fullResponse = ''; 575 1054 576 - if (aiResponse.reasoning) { 577 - const cleanedReasoning = aiResponse.reasoning 578 - .split('\n') 579 - .map((line) => line.trim()) 580 - .filter((line) => line) 581 - .join('\n'); 1055 + if (aiResponse.reasoning) { 1056 + const cleanedReasoning = aiResponse.reasoning 1057 + .split('\n') 1058 + .map((line: string) => line.trim()) 1059 + .filter((line: string) => line) 1060 + .join('\n'); 582 1061 583 - const formattedReasoning = cleanedReasoning 584 - .split('\n') 585 - .map((line) => `> ${line}`) 586 - .join('\n'); 1062 + const formattedReasoning = cleanedReasoning 1063 + .split('\n') 1064 + .map((line: string) => `> ${line}`) 1065 + .join('\n'); 587 1066 588 - fullResponse = `${formattedReasoning}\n\n${aiResponse.content}`; 589 - aiResponse.content = ''; 590 - } 1067 + fullResponse = `${formattedReasoning}\n\n${aiResponse.content}`; 1068 + aiResponse.content = ''; 1069 + } 591 1070 592 - fullResponse += aiResponse.content; 1071 + fullResponse += aiResponse.content; 593 1072 594 - const { getUnallowedWordCategory } = await import('@/utils/validation'); 595 - const category = getUnallowedWordCategory(fullResponse); 596 - if (category) { 597 - logger.warn(`AI response contained unallowed words in category: ${category}`); 598 - await interaction.editReply( 599 - 'Sorry, I cannot provide that response as it contains prohibited content. Please try a different prompt.', 600 - ); 601 - return; 602 - } 1073 + try { 1074 + if (aiResponse.citations && aiResponse.citations.length && fullResponse) { 1075 + fullResponse = fullResponse.replace(/\[(\d+)\](?!\()/g, (match: string, numStr: string) => { 1076 + const idx = parseInt(numStr, 10) - 1; 1077 + const url = aiResponse.citations![idx]; 1078 + if (typeof url === 'string' && url.trim()) { 1079 + return `[${numStr}](${url.trim()})`; 1080 + } 1081 + return match; 1082 + }); 1083 + } 1084 + } catch (e) { 1085 + logger.warn('Failed to inline citation sources', e); 1086 + } 603 1087 604 - const urlProcessedResponse = processUrls(fullResponse); 605 - const chunks = splitResponseIntoChunks(urlProcessedResponse); 1088 + if (aiResponse.toolResults) { 1089 + try { 1090 + const toolResults = Array.isArray(aiResponse.toolResults) 1091 + ? aiResponse.toolResults 1092 + : [aiResponse.toolResults]; 606 1093 607 - try { 1094 + for (const result of toolResults) { 1095 + try { 1096 + let toolResult; 1097 + if (typeof result === 'string') { 1098 + try { 1099 + toolResult = JSON.parse(result); 1100 + } catch (parseError) { 1101 + logger.error(`[AI] Error parsing tool result JSON:`, { 1102 + error: parseError, 1103 + result: result.substring(0, 200) + '...', 1104 + }); 1105 + continue; 1106 + } 1107 + } else { 1108 + toolResult = result; 1109 + } 1110 + 1111 + if ((toolResult.type === 'cat' || toolResult.type === 'dog') && toolResult.url) { 1112 + let cleanContent = aiResponse.content || ''; 1113 + if (toolResult.url) { 1114 + cleanContent = cleanContent.replace(/!\[[^\]]*\]\([^)]*\)/g, '').trim(); 1115 + cleanContent = cleanContent.replace(toolResult.url, '').trim(); 1116 + } 1117 + 1118 + await interaction.editReply({ 1119 + content: cleanContent || undefined, 1120 + files: [ 1121 + { 1122 + attachment: toolResult.url, 1123 + name: `${toolResult.type}.jpg`, 1124 + }, 1125 + ], 1126 + }); 1127 + return; 1128 + } 1129 + } catch (parseError) { 1130 + logger.error('Error parsing individual tool result:', parseError); 1131 + } 1132 + } 1133 + } catch (error) { 1134 + logger.error('Error processing tool results:', error); 1135 + } 1136 + } 1137 + 1138 + const { getUnallowedWordCategory } = await import('@/utils/validation'); 1139 + const category = getUnallowedWordCategory(fullResponse); 1140 + if (category) { 1141 + logger.warn(`AI response contained unallowed words in category: ${category}`); 1142 + await interaction.editReply( 1143 + 'Sorry, I cannot provide that response as it contains prohibited content. Please try a different prompt.', 1144 + ); 1145 + return; 1146 + } 1147 + 1148 + const urlProcessedResponse = processUrls(fullResponse); 1149 + const chunks = splitResponseIntoChunks(urlProcessedResponse); 1150 + 608 1151 await interaction.editReply(chunks[0]); 609 1152 610 1153 for (let i = 1; i < chunks.length; i++) { 611 1154 await interaction.followUp({ 612 1155 content: chunks[i], 613 - flags: MessageFlags.Ephemeral, 1156 + flags: MessageFlags.SuppressNotifications, 614 1157 }); 615 1158 } 616 - } catch { 1159 + } catch (error) { 1160 + logger.error('Error in sendAIResponse:', error); 617 1161 try { 618 - const fallbackMessage = `${chunks[0]}\n\n*โŒ ${await client.getLocaleText('commands.ai.errors.toolong', interaction.locale)}*`; 619 - await interaction.editReply(fallbackMessage); 620 - } catch { 621 - logger.error('Failed to send AI response fallback message'); 1162 + await interaction.editReply('An error occurred while processing your request.'); 1163 + } catch (editError) { 1164 + logger.error('Failed to send error message:', editError); 1165 + } 1166 + return; 1167 + } 1168 + 1169 + if (aiResponse.toolResults) { 1170 + try { 1171 + const toolResult = JSON.parse(aiResponse.toolResults); 1172 + 1173 + if (toolResult.alreadyResponded && !aiResponse.content) { 1174 + return; 1175 + } 1176 + 1177 + if (toolResult.type === 'command') { 1178 + if (toolResult.image) { 1179 + const embed = new EmbedBuilder().setImage(toolResult.image).setColor(0x8a2be2); 1180 + 1181 + if (toolResult.title) { 1182 + embed.setTitle(toolResult.title); 1183 + } 1184 + if (toolResult.source) { 1185 + embed.setFooter({ text: `Source: ${toolResult.source}` }); 1186 + } 1187 + 1188 + try { 1189 + await interaction.followUp({ 1190 + embeds: [embed], 1191 + flags: MessageFlags.SuppressNotifications, 1192 + }); 1193 + return; 1194 + } catch (error) { 1195 + logger.error('Failed to send embed with source:', error); 1196 + return; 1197 + } 1198 + } 1199 + 1200 + if (toolResult.success && toolResult.data) { 1201 + const components = toolResult.data.components || []; 1202 + let imageUrl: string | null = null; 1203 + let caption = ''; 1204 + 1205 + for (const component of components) { 1206 + if (component.type === 12 && component.items?.[0]?.media?.url) { 1207 + imageUrl = component.items[0].media.url; 1208 + break; 1209 + } 1210 + } 1211 + 1212 + for (const component of components) { 1213 + if (component.type === 10 && component.content) { 1214 + caption = component.content; 1215 + break; 1216 + } 1217 + } 1218 + 1219 + if (imageUrl) { 1220 + await interaction.followUp({ 1221 + content: caption || undefined, 1222 + files: [imageUrl], 1223 + flags: MessageFlags.SuppressNotifications, 1224 + }); 1225 + return; 1226 + } 1227 + } 1228 + 1229 + if (aiResponse.toolResults) { 1230 + await interaction.followUp({ 1231 + content: aiResponse.toolResults, 1232 + flags: MessageFlags.SuppressNotifications, 1233 + }); 1234 + } 1235 + } 1236 + } catch (error) { 1237 + logger.error('Error processing tool results:', error); 1238 + try { 1239 + await interaction.followUp({ 1240 + content: 'An error occurred while processing the tool results.', 1241 + flags: MessageFlags.SuppressNotifications, 1242 + }); 1243 + } catch (followUpError) { 1244 + logger.error('Failed to send error message:', followUpError); 1245 + } 622 1246 } 623 1247 } 624 1248 } 1249 + 1250 + export type { ConversationMessage, AIResponse }; 625 1251 626 1252 export { 627 1253 makeAIRequest, 628 1254 getApiConfiguration, 629 1255 buildSystemPrompt, 630 1256 buildConversation, 631 - sendAIResponse, 632 1257 getUserCredentials, 633 1258 incrementAndCheckDailyLimit, 634 1259 incrementAndCheckServerDailyLimit, 635 1260 splitResponseIntoChunks, 636 1261 }; 637 1262 638 - export type { ConversationMessage, AIResponse }; 1263 + interface AICommand { 1264 + data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; 1265 + execute: ( 1266 + client: BotClient, 1267 + interaction: ChatInputCommandInteraction, 1268 + ) => Promise<void | InteractionResponse<boolean>>; 1269 + } 639 1270 640 - export default { 1271 + const aiCommand: AICommand = { 641 1272 data: new SlashCommandBuilder() 642 1273 .setName('ai') 643 1274 .setNameLocalizations({ ··· 660 1291 .addStringOption((option) => 661 1292 option 662 1293 .setName('prompt') 663 - .setNameLocalizations({ 664 - 'es-ES': 'mensaje', 665 - 'es-419': 'mensaje', 666 - 'en-US': 'prompt', 667 - }) 668 1294 .setDescription('Your message to the AI') 669 1295 .setDescriptionLocalizations({ 670 1296 'es-ES': 'Tu mensaje para la IA', ··· 674 1300 .setRequired(true), 675 1301 ) 676 1302 .addBooleanOption((option) => 677 - option.setName('use_custom_api').setDescription('Use your own API key?').setRequired(false), 1303 + option.setName('custom_setup').setDescription('Use your own API key?').setRequired(false), 678 1304 ) 679 1305 .addBooleanOption((option) => 680 1306 option.setName('reset').setDescription('Reset your AI chat history').setRequired(false), ··· 685 1311 686 1312 if (pendingRequests.has(userId)) { 687 1313 const pending = pendingRequests.get(userId); 688 - if (pending && Date.now() - pending.timestamp > 30000) { 689 - pendingRequests.delete(userId); 690 - } else { 1314 + const isProcessing = pending?.status === 'processing'; 1315 + const isExpired = pending ? Date.now() - pending.createdAt > 30000 : true; 1316 + if (isProcessing && !isExpired) { 691 1317 return interaction.reply({ 692 1318 content: await client.getLocaleText('commands.ai.request.inprogress', interaction.locale), 693 1319 flags: MessageFlags.Ephemeral, 694 1320 }); 695 1321 } 1322 + pendingRequests.delete(userId); 696 1323 } 697 1324 698 1325 try { 699 - const useCustomApi = interaction.options.getBoolean('use_custom_api'); 1326 + const customSetup = interaction.options.getBoolean('custom_setup'); 700 1327 const prompt = interaction.options.getString('prompt')!; 701 1328 const reset = interaction.options.getBoolean('reset'); 702 1329 703 - pendingRequests.set(userId, { interaction, prompt, timestamp: Date.now() }); 1330 + pendingRequests.set(userId, { 1331 + interaction, 1332 + prompt, 1333 + createdAt: Date.now(), 1334 + status: 'awaiting', 1335 + }); 704 1336 705 1337 if (reset) { 706 1338 userConversations.delete(userId); ··· 712 1344 return; 713 1345 } 714 1346 715 - if (useCustomApi === false) { 716 - await setUserApiKey(userId, null, null, null); 1347 + if (customSetup !== null) { 1348 + const { apiKey } = await getUserCredentials(interaction.user.id); 1349 + 1350 + if (customSetup) { 1351 + if (!apiKey) { 1352 + const setupUrl = process.env.FRONTEND_URL 1353 + ? `${process.env.FRONTEND_URL}/api-keys` 1354 + : 'the API keys page'; 1355 + 1356 + return interaction.reply({ 1357 + content: `๐Ÿ”‘ Please set up your API key first by visiting: ${setupUrl}\n\nAfter setting up your API key, you can use the AI command with your custom key.`, 1358 + flags: MessageFlags.Ephemeral, 1359 + }); 1360 + } 1361 + const setupUrl = process.env.FRONTEND_URL 1362 + ? `${process.env.FRONTEND_URL}/api-keys` 1363 + : 'the API keys page'; 1364 + 1365 + await interaction.reply({ 1366 + content: `โœ… You're already using a custom API key. To change your API key settings, please visit: ${setupUrl}`, 1367 + flags: MessageFlags.Ephemeral, 1368 + }); 1369 + return; 1370 + } 1371 + await setUserApiKey(interaction.user.id, null, null, null); 717 1372 userConversations.delete(userId); 718 1373 await interaction.reply({ 719 1374 content: await client.getLocaleText('commands.ai.defaultapi', interaction.locale), ··· 723 1378 return; 724 1379 } 725 1380 726 - const { apiKey } = await getUserCredentials(userId); 727 - if (useCustomApi && !apiKey) { 728 - const modal = new ModalBuilder() 729 - .setCustomId('apiCredentials') 730 - .setTitle(await client.getLocaleText('commands.ai.modal.title', interaction.locale)); 1381 + await processAIRequest(client, interaction); 1382 + } catch (error) { 1383 + logger.error('Error in AI command:', error); 1384 + const errorMessage = `โŒ An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`; 731 1385 732 - const apiKeyInput = new TextInputBuilder() 733 - .setCustomId('apiKey') 734 - .setLabel(await client.getLocaleText('commands.ai.modal.apikey', interaction.locale)) 735 - .setStyle(TextInputStyle.Short) 736 - .setPlaceholder( 737 - await client.getLocaleText('commands.ai.modal.apikeyplaceholder', interaction.locale), 738 - ) 739 - .setRequired(true); 1386 + try { 1387 + if (interaction.replied || interaction.deferred) { 1388 + await interaction.editReply({ content: errorMessage }); 1389 + } else { 1390 + await interaction.reply({ 1391 + content: errorMessage, 1392 + flags: MessageFlags.Ephemeral, 1393 + }); 1394 + } 1395 + } catch (replyError) { 1396 + logger.error('Failed to send error message:', replyError); 1397 + } 740 1398 741 - const apiUrlInput = new TextInputBuilder() 742 - .setCustomId('apiUrl') 743 - .setLabel(await client.getLocaleText('commands.ai.modal.apiurl', interaction.locale)) 744 - .setStyle(TextInputStyle.Short) 745 - .setPlaceholder( 746 - await client.getLocaleText('commands.ai.modal.apiurlplaceholder', interaction.locale), 747 - ) 748 - .setRequired(true); 749 - 750 - const modelInput = new TextInputBuilder() 751 - .setCustomId('model') 752 - .setLabel(await client.getLocaleText('commands.ai.modal.model', interaction.locale)) 753 - .setStyle(TextInputStyle.Short) 754 - .setPlaceholder( 755 - await client.getLocaleText('commands.ai.modal.modelplaceholder', interaction.locale), 756 - ) 757 - .setRequired(true); 758 - 759 - modal.addComponents( 760 - new ActionRowBuilder<TextInputBuilder>().addComponents(apiKeyInput), 761 - new ActionRowBuilder<TextInputBuilder>().addComponents(apiUrlInput), 762 - new ActionRowBuilder<TextInputBuilder>().addComponents(modelInput), 763 - ); 764 - 765 - await interaction.showModal(modal); 766 - } else { 767 - await interaction.deferReply(); 768 - await processAIRequest(client, interaction); 769 - } 770 - } catch { 1399 + const userId = getInvokerId(interaction); 771 1400 pendingRequests.delete(userId); 772 - const errorMessage = await client.getLocaleText('failedrequest', interaction.locale); 773 - if (!interaction.replied && !interaction.deferred) { 774 - await interaction.reply({ content: errorMessage, flags: MessageFlags.Ephemeral }); 775 - } else { 776 - await interaction.editReply({ content: errorMessage }); 777 - } 778 1401 } 779 1402 }, 1403 + }; 780 1404 781 - async handleModal(client: BotClient, interaction: ModalSubmitInteraction) { 782 - try { 783 - if (interaction.customId === 'apiCredentials') { 784 - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 785 - 786 - const userId = getInvokerId(interaction); 787 - const pendingRequest = pendingRequests.get(userId); 788 - 789 - if (!pendingRequest) { 790 - return interaction.editReply( 791 - await client.getLocaleText('commands.ai.nopendingrequest', interaction.locale), 792 - ); 793 - } 794 - 795 - const { interaction: originalInteraction } = pendingRequest; 796 - const apiKey = interaction.fields.getTextInputValue('apiKey').trim(); 797 - const apiUrl = interaction.fields.getTextInputValue('apiUrl').trim(); 798 - const model = interaction.fields.getTextInputValue('model').trim(); 799 - 800 - let parsedUrl; 801 - try { 802 - parsedUrl = new URL(apiUrl); 803 - } catch { 804 - await interaction.editReply( 805 - 'API URL is invalid. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).', 806 - ); 807 - return; 808 - } 809 - 810 - if (!ALLOWED_API_HOSTS.includes(parsedUrl.hostname)) { 811 - await interaction.editReply( 812 - 'API URL not allowed. Please use a supported API endpoint (OpenAI, OpenRouter, or Google Gemini).', 813 - ); 814 - return; 815 - } 816 - 817 - await interaction.editReply( 818 - await client.getLocaleText('commands.ai.testing', interaction.locale), 819 - ); 820 - const testResult = await testApiKey(apiKey, model, apiUrl); 821 - 822 - if (!testResult.success) { 823 - const errorMessage = await client.getLocaleText( 824 - 'commands.ai.testfailed', 825 - interaction.locale, 826 - ); 827 - await interaction.editReply( 828 - errorMessage.replace('{error}', testResult.error || 'Unknown error'), 829 - ); 830 - return; 831 - } 832 - 833 - await setUserApiKey(userId, apiKey, model, apiUrl); 834 - await interaction.editReply( 835 - await client.getLocaleText('commands.ai.testsuccess', interaction.locale), 836 - ); 837 - 838 - if (!originalInteraction.deferred && !originalInteraction.replied) { 839 - await originalInteraction.deferReply(); 840 - } 841 - await processAIRequest(client, originalInteraction); 842 - } 843 - } catch { 844 - await interaction.editReply({ 845 - content: await client.getLocaleText('failedrequest', interaction.locale), 846 - }); 847 - } finally { 848 - pendingRequests.delete(getInvokerId(interaction)); 849 - } 850 - }, 851 - } as unknown as SlashCommandProps; 852 - 853 - function getInvokerId(interaction: ChatInputCommandInteraction | ModalSubmitInteraction) { 854 - if (interaction.inGuild()) { 855 - return `${interaction.guildId}-${interaction.user.id}`; 856 - } 857 - return interaction.user.id; 858 - } 1405 + export default aiCommand as unknown as SlashCommandProps;
+424
src/commands/utilities/stocks.ts
··· 1 + import { 2 + SlashCommandBuilder, 3 + MessageFlags, 4 + InteractionContextType, 5 + ApplicationIntegrationType, 6 + EmbedBuilder, 7 + ActionRowBuilder, 8 + ButtonBuilder, 9 + ButtonStyle, 10 + AttachmentBuilder, 11 + type MessageActionRowComponentBuilder, 12 + } from 'discord.js'; 13 + import { SlashCommandProps } from '@/types/command'; 14 + import logger from '@/utils/logger'; 15 + import { sanitizeInput } from '@/utils/validation'; 16 + import { 17 + getTickerOverview, 18 + getAggregateSeries, 19 + buildBrandingUrl, 20 + sanitizeTickerInput, 21 + StockTimeframe, 22 + } from '@/services/massive'; 23 + import { renderStockCandles } from '@/utils/stockChart'; 24 + import { 25 + createCooldownManager, 26 + checkCooldown, 27 + setCooldown, 28 + createCooldownResponse, 29 + } from '@/utils/cooldown'; 30 + import { createCommandLogger } from '@/utils/commandLogger'; 31 + import { createErrorHandler } from '@/utils/errorHandler'; 32 + import * as config from '@/config'; 33 + import BotClient from '@/services/Client'; 34 + 35 + const cooldownManager = createCooldownManager('stocks', 5000); 36 + const commandLogger = createCommandLogger('stocks'); 37 + const errorHandler = createErrorHandler('stocks'); 38 + 39 + const DEFAULT_TIMEFRAME: StockTimeframe = '1d'; 40 + const SUPPORTED_TIMEFRAMES: StockTimeframe[] = ['1d', '5d', '1m', '3m', '1y']; 41 + const BUTTON_PREFIX = 'stocks_tf'; 42 + const MAX_DESCRIPTION_LENGTH = 350; 43 + 44 + const TIMEFRAME_LABEL_KEYS: Record<StockTimeframe, string> = { 45 + '1d': 'commands.stocks.buttons.timeframes.1d', 46 + '5d': 'commands.stocks.buttons.timeframes.5d', 47 + '1m': 'commands.stocks.buttons.timeframes.1m', 48 + '3m': 'commands.stocks.buttons.timeframes.3m', 49 + '1y': 'commands.stocks.buttons.timeframes.1y', 50 + }; 51 + 52 + const compactNumber = new Intl.NumberFormat('en-US', { 53 + notation: 'compact', 54 + maximumFractionDigits: 2, 55 + }); 56 + 57 + function getCurrencyFormatter(code?: string) { 58 + const currency = code && code.length === 3 ? code : 'USD'; 59 + try { 60 + return new Intl.NumberFormat('en-US', { 61 + style: 'currency', 62 + currency, 63 + maximumFractionDigits: currency === 'JPY' ? 0 : 2, 64 + }); 65 + } catch { 66 + return new Intl.NumberFormat('en-US', { 67 + style: 'currency', 68 + currency: 'USD', 69 + maximumFractionDigits: 2, 70 + }); 71 + } 72 + } 73 + 74 + function formatCurrency(value?: number, currency?: string) { 75 + if (typeof value !== 'number' || Number.isNaN(value)) { 76 + return 'โ€”'; 77 + } 78 + return getCurrencyFormatter(currency).format(value); 79 + } 80 + 81 + function formatNumber(value?: number) { 82 + if (typeof value !== 'number' || Number.isNaN(value)) { 83 + return 'โ€”'; 84 + } 85 + return compactNumber.format(value); 86 + } 87 + 88 + function truncateDescription(description?: string) { 89 + if (!description) return undefined; 90 + const clean = sanitizeInput(description); 91 + if (clean.length <= MAX_DESCRIPTION_LENGTH) { 92 + return clean; 93 + } 94 + return `${clean.slice(0, MAX_DESCRIPTION_LENGTH)}โ€ฆ`; 95 + } 96 + 97 + function resolveCurrencyCode(value?: string) { 98 + if (!value) return 'USD'; 99 + const normalized = value.trim().toUpperCase(); 100 + if (normalized.length === 3) { 101 + return normalized; 102 + } 103 + return 'USD'; 104 + } 105 + 106 + function toValidDate(value?: number | string | null) { 107 + if (value === null || value === undefined) { 108 + return undefined; 109 + } 110 + 111 + if (typeof value === 'number' && Number.isFinite(value)) { 112 + const date = new Date(value); 113 + return Number.isNaN(date.getTime()) ? undefined : date; 114 + } 115 + 116 + if (typeof value === 'string' && value.trim().length > 0) { 117 + const date = new Date(value); 118 + return Number.isNaN(date.getTime()) ? undefined : date; 119 + } 120 + 121 + return undefined; 122 + } 123 + 124 + interface StocksRenderOptions { 125 + client: BotClient; 126 + locale: string; 127 + ticker: string; 128 + timeframe: StockTimeframe; 129 + userId: string; 130 + } 131 + 132 + async function buildTimeframeButtons( 133 + client: BotClient, 134 + locale: string, 135 + active: StockTimeframe, 136 + userId: string, 137 + ticker: string, 138 + ) { 139 + const row = new ActionRowBuilder<MessageActionRowComponentBuilder>(); 140 + 141 + for (const timeframe of SUPPORTED_TIMEFRAMES) { 142 + const label = await client.getLocaleText(TIMEFRAME_LABEL_KEYS[timeframe], locale); 143 + row.addComponents( 144 + new ButtonBuilder() 145 + .setCustomId(`${BUTTON_PREFIX}:${userId}:${ticker}:${timeframe}`) 146 + .setLabel(label.toUpperCase()) 147 + .setStyle(timeframe === active ? ButtonStyle.Primary : ButtonStyle.Secondary), 148 + ); 149 + } 150 + 151 + return row; 152 + } 153 + 154 + export async function renderStocksView(options: StocksRenderOptions) { 155 + const normalizedTicker = sanitizeTickerInput(options.ticker); 156 + if (!normalizedTicker) { 157 + const error = new Error('STOCKS_TICKER_NOT_FOUND'); 158 + throw error; 159 + } 160 + 161 + const overview = await getTickerOverview(normalizedTicker); 162 + if (!overview.detail) { 163 + const error = new Error('STOCKS_TICKER_NOT_FOUND'); 164 + throw error; 165 + } 166 + 167 + const aggregates = await getAggregateSeries(normalizedTicker, options.timeframe); 168 + 169 + const detail = overview.detail; 170 + const snapshot = overview.snapshot; 171 + const lastPrice = snapshot?.lastTrade?.p ?? snapshot?.day?.c ?? snapshot?.prevDay?.c; 172 + const prevClose = snapshot?.prevDay?.c; 173 + const changeValue = 174 + snapshot?.todaysChange ?? (lastPrice && prevClose ? lastPrice - prevClose : undefined); 175 + const changePercent = 176 + snapshot?.todaysChangePerc ?? 177 + (changeValue && prevClose ? (changeValue / prevClose) * 100 : undefined); 178 + const trend = 179 + typeof changeValue === 'number' 180 + ? changeValue === 0 181 + ? 'neutral' 182 + : changeValue > 0 183 + ? 'up' 184 + : 'down' 185 + : 'neutral'; 186 + const color = trend === 'up' ? 0x1ac486 : trend === 'down' ? 0xff6b6b : 0x5865f2; 187 + const chartBuffer = aggregates.length 188 + ? await renderStockCandles(aggregates, options.timeframe) 189 + : undefined; 190 + 191 + const [ 192 + priceLabel, 193 + changeLabel, 194 + rangeLabel, 195 + volumeLabel, 196 + prevCloseLabel, 197 + marketCapLabel, 198 + providedBy, 199 + ] = await Promise.all([ 200 + options.client.getLocaleText('commands.stocks.labels.price', options.locale), 201 + options.client.getLocaleText('commands.stocks.labels.change', options.locale), 202 + options.client.getLocaleText('commands.stocks.labels.dayrange', options.locale), 203 + options.client.getLocaleText('commands.stocks.labels.volume', options.locale), 204 + options.client.getLocaleText('commands.stocks.labels.prevclose', options.locale), 205 + options.client.getLocaleText('commands.stocks.labels.marketcap', options.locale), 206 + options.client.getLocaleText('providedby', options.locale), 207 + ]); 208 + 209 + const currencySymbol = resolveCurrencyCode(detail.currency_name); 210 + const description = truncateDescription(detail.description); 211 + const dayLow = snapshot?.day?.l ?? snapshot?.prevDay?.l; 212 + const dayHigh = snapshot?.day?.h ?? snapshot?.prevDay?.h; 213 + const thumbnail = buildBrandingUrl(detail.branding?.icon_url ?? detail.branding?.logo_url); 214 + const footerText = `${providedBy} Massive.com`; 215 + 216 + const embed = new EmbedBuilder() 217 + .setColor(color) 218 + .setTitle(`${normalizedTicker} โ€ข ${detail.name}`) 219 + .setFooter({ text: footerText }); 220 + 221 + const timestampDate = toValidDate(snapshot?.updated); 222 + embed.setTimestamp(timestampDate ?? new Date()); 223 + 224 + if (description) { 225 + embed.setDescription(description); 226 + } 227 + 228 + if (thumbnail) { 229 + embed.setThumbnail(thumbnail); 230 + } 231 + 232 + let files: AttachmentBuilder[] = []; 233 + if (chartBuffer) { 234 + const attachmentName = `stocks-${normalizedTicker}-${options.timeframe}.png`; 235 + const attachment = new AttachmentBuilder(chartBuffer, { name: attachmentName }); 236 + embed.setImage(`attachment://${attachmentName}`); 237 + files = [attachment]; 238 + } else { 239 + embed.addFields({ 240 + name: '\u200B', 241 + value: await options.client.getLocaleText('commands.stocks.labels.nochart', options.locale), 242 + }); 243 + } 244 + 245 + embed.addFields( 246 + { 247 + name: priceLabel, 248 + value: formatCurrency(lastPrice, currencySymbol), 249 + inline: true, 250 + }, 251 + { 252 + name: changeLabel, 253 + value: 254 + typeof changeValue === 'number' 255 + ? changePercent 256 + ? `${formatCurrency(changeValue, currencySymbol)} (${changePercent.toFixed(2)}%)` 257 + : formatCurrency(changeValue, currencySymbol) 258 + : 'โ€”', 259 + inline: true, 260 + }, 261 + { 262 + name: rangeLabel, 263 + value: `${formatCurrency(dayLow, currencySymbol)} - ${formatCurrency(dayHigh, currencySymbol)}`, 264 + inline: true, 265 + }, 266 + { 267 + name: volumeLabel, 268 + value: formatNumber(snapshot?.day?.v ?? snapshot?.prevDay?.v), 269 + inline: true, 270 + }, 271 + { 272 + name: prevCloseLabel, 273 + value: formatCurrency(prevClose, currencySymbol), 274 + inline: true, 275 + }, 276 + { 277 + name: marketCapLabel, 278 + value: formatNumber(detail.market_cap), 279 + inline: true, 280 + }, 281 + ); 282 + 283 + const buttons = await buildTimeframeButtons( 284 + options.client, 285 + options.locale, 286 + options.timeframe, 287 + options.userId, 288 + normalizedTicker, 289 + ); 290 + 291 + return { 292 + embeds: [embed], 293 + components: [buttons], 294 + files, 295 + }; 296 + } 297 + 298 + export default { 299 + data: new SlashCommandBuilder() 300 + .setName('stocks') 301 + .setDescription('Track stock prices and view quick charts') 302 + .addStringOption((option) => 303 + option 304 + .setName('ticker') 305 + .setDescription('The stock ticker symbol (e.g., AAPL, TSLA)') 306 + .setRequired(true) 307 + .setMaxLength(15), 308 + ) 309 + .addStringOption((option) => 310 + option 311 + .setName('range') 312 + .setDescription('Initial timeframe for the chart') 313 + .addChoices( 314 + { name: '1D', value: '1d' }, 315 + { name: '5D', value: '5d' }, 316 + { name: '1M', value: '1m' }, 317 + { name: '3M', value: '3m' }, 318 + { name: '1Y', value: '1y' }, 319 + ), 320 + ) 321 + .setContexts([ 322 + InteractionContextType.BotDM, 323 + InteractionContextType.Guild, 324 + InteractionContextType.PrivateChannel, 325 + ]) 326 + .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 327 + 328 + async execute(client, interaction) { 329 + try { 330 + const cooldownCheck = await checkCooldown( 331 + cooldownManager, 332 + interaction.user.id, 333 + client, 334 + interaction.locale, 335 + ); 336 + if (cooldownCheck.onCooldown) { 337 + return interaction.reply(createCooldownResponse(cooldownCheck.message!)); 338 + } 339 + 340 + if (!config.MASSIVE_API_KEY) { 341 + const msg = await client.getLocaleText( 342 + 'commands.stocks.errors.noapikey', 343 + interaction.locale, 344 + ); 345 + return interaction.reply({ content: msg, flags: MessageFlags.Ephemeral }); 346 + } 347 + 348 + setCooldown(cooldownManager, interaction.user.id); 349 + 350 + const tickerInput = interaction.options.getString('ticker', true); 351 + const timeframeInput = 352 + (interaction.options.getString('range') as StockTimeframe | null) ?? DEFAULT_TIMEFRAME; 353 + const ticker = sanitizeTickerInput(tickerInput); 354 + 355 + if (!ticker) { 356 + const notFound = await client.getLocaleText( 357 + 'commands.stocks.errors.notfound', 358 + interaction.locale, 359 + { 360 + ticker: tickerInput, 361 + }, 362 + ); 363 + return interaction.reply({ content: notFound, flags: MessageFlags.Ephemeral }); 364 + } 365 + 366 + commandLogger.logFromInteraction( 367 + interaction, 368 + `ticker: ${ticker} timeframe: ${timeframeInput}`, 369 + ); 370 + 371 + await interaction.deferReply(); 372 + 373 + try { 374 + const response = await renderStocksView({ 375 + client, 376 + locale: interaction.locale, 377 + ticker, 378 + timeframe: timeframeInput, 379 + userId: interaction.user.id, 380 + }); 381 + 382 + await interaction.editReply(response); 383 + } catch (error) { 384 + if ((error as Error).message === 'STOCKS_TICKER_NOT_FOUND') { 385 + const notFound = await client.getLocaleText( 386 + 'commands.stocks.errors.notfound', 387 + interaction.locale, 388 + { ticker }, 389 + ); 390 + await interaction.editReply({ content: notFound, components: [] }); 391 + return; 392 + } 393 + 394 + await errorHandler({ 395 + interaction, 396 + client, 397 + error: error as Error, 398 + userId: interaction.user.id, 399 + username: interaction.user.tag, 400 + }); 401 + } 402 + } catch (error) { 403 + logger.error('Unexpected error in stocks command:', error); 404 + if (!interaction.replied && !interaction.deferred) { 405 + await interaction.reply({ 406 + content: await client.getLocaleText('unexpectederror', interaction.locale), 407 + flags: MessageFlags.Ephemeral, 408 + }); 409 + } else if (interaction.deferred) { 410 + const errorMsg = await client.getLocaleText('unexpectederror', interaction.locale); 411 + await interaction.editReply({ content: errorMsg }); 412 + } 413 + } 414 + }, 415 + } as SlashCommandProps; 416 + 417 + export function parseStocksButtonId(customId: string) { 418 + if (!customId.startsWith(`${BUTTON_PREFIX}:`)) return null; 419 + const [, userId, ticker, timeframe] = customId.split(':'); 420 + if (!userId || !ticker || !SUPPORTED_TIMEFRAMES.includes(timeframe as StockTimeframe)) { 421 + return null; 422 + } 423 + return { userId, ticker, timeframe: timeframe as StockTimeframe }; 424 + }
+2 -2
src/commands/utilities/wiki.ts
··· 24 24 25 25 const MAX_EXTRACT_LENGTH = 2000; 26 26 27 - async function searchWikipedia(query: string, locale = 'en') { 27 + export async function searchWikipedia(query: string, locale = 'en') { 28 28 const wikiLang = locale.startsWith('es') ? 'es' : 'en'; 29 29 const searchUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&srlimit=1`; 30 30 ··· 46 46 }; 47 47 } 48 48 49 - async function getArticleSummary(pageId: number, wikiLang = 'en') { 49 + export async function getArticleSummary(pageId: number, wikiLang = 'en') { 50 50 const summaryUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&prop=extracts|pageimages&exintro&explaintext&format=json&pithumbsize=300&pageids=${pageId}`; 51 51 const response = await fetch(summaryUrl); 52 52
+4
src/config/index.ts
··· 7 7 CLIENT_ID: process.env.CLIENT_ID, 8 8 DATABASE_URL: process.env.DATABASE_URL, 9 9 API_KEY_ENCRYPTION_SECRET: process.env.API_KEY_ENCRYPTION_SECRET, 10 + CLIENT_SECRET: process.env.CLIENT_SECRET, 11 + REDIRECT_URI: process.env.REDIRECT_URI, 10 12 }; 11 13 12 14 for (const [key, value] of Object.entries(requiredEnvVars)) { ··· 23 25 export const DATABASE_URL = process.env.DATABASE_URL!; 24 26 export const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; 25 27 export const OPENWEATHER_API_KEY = process.env.OPENWEATHER_API_KEY; 28 + export const MASSIVE_API_KEY = process.env.MASSIVE_API_KEY; 29 + export const MASSIVE_API_BASE_URL = process.env.MASSIVE_API_BASE_URL ?? 'https://api.massive.com'; 26 30 export const SOURCE_COMMIT = process.env.SOURCE_COMMIT; 27 31 export const TOKEN = process.env.TOKEN!; 28 32 export const CLIENT_ID = process.env.CLIENT_ID!;
+48 -1
src/events/interactionCreate.ts
··· 1 1 import { browserHeaders } from '@/constants/index'; 2 2 import BotClient from '@/services/Client'; 3 + import * as config from '@/config'; 4 + import { renderStocksView, parseStocksButtonId } from '@/commands/utilities/stocks'; 3 5 import { RandomReddit } from '@/types/base'; 4 6 import { RemindCommandProps } from '@/types/command'; 5 7 import logger from '@/utils/logger'; ··· 89 91 if (remind && remind.handleModal) { 90 92 await remind.handleModal(this.client, i); 91 93 } 92 - } else if (i.customId === 'apiCredentials') { 94 + } else if (i.customId.startsWith('apiCredentials')) { 93 95 const ai = this.client.commands.get('ai'); 94 96 if (ai && 'handleModal' in ai) { 95 97 await (ai as unknown as RemindCommandProps).handleModal(this.client, i); ··· 127 129 } 128 130 ).handleButton(this.client, i); 129 131 } 132 + } 133 + 134 + const stocksPayload = parseStocksButtonId(i.customId); 135 + if (stocksPayload) { 136 + if (!config.MASSIVE_API_KEY) { 137 + const message = await this.client.getLocaleText( 138 + 'commands.stocks.errors.noapikey', 139 + i.locale, 140 + ); 141 + return await i.reply({ content: message, flags: MessageFlags.Ephemeral }); 142 + } 143 + 144 + if (stocksPayload.userId !== i.user.id) { 145 + const unauthorized = 146 + (await this.client.getLocaleText('commands.stocks.errors.unauthorized', i.locale)) || 147 + 'Only the person who used /stocks can use these buttons.'; 148 + return await i.reply({ content: unauthorized, flags: MessageFlags.Ephemeral }); 149 + } 150 + 151 + await i.deferUpdate(); 152 + 153 + try { 154 + const response = await renderStocksView({ 155 + client: this.client, 156 + locale: i.locale, 157 + ticker: stocksPayload.ticker, 158 + timeframe: stocksPayload.timeframe, 159 + userId: stocksPayload.userId, 160 + }); 161 + await i.editReply(response); 162 + } catch (error) { 163 + if ((error as Error).message === 'STOCKS_TICKER_NOT_FOUND') { 164 + const notFound = await this.client.getLocaleText( 165 + 'commands.stocks.errors.notfound', 166 + i.locale, 167 + { ticker: stocksPayload.ticker }, 168 + ); 169 + await i.editReply({ content: notFound, components: [] }); 170 + } else { 171 + logger.error('Error updating stocks view:', error); 172 + const failMsg = await this.client.getLocaleText('failedrequest', i.locale); 173 + await i.editReply({ content: failMsg, components: [] }); 174 + } 175 + } 176 + return; 130 177 } 131 178 132 179 const originalUser = i.message.interaction!.user;
+621 -74
src/events/messageCreate.ts
··· 1 - import { Message, ChannelType } from 'discord.js'; 1 + import { Message, ChannelType, type ChatInputCommandInteraction } from 'discord.js'; 2 2 import BotClient from '@/services/Client'; 3 3 import logger from '@/utils/logger'; 4 4 import { 5 5 makeAIRequest, 6 6 getApiConfiguration, 7 - buildSystemPrompt, 7 + buildSystemPrompt as originalBuildSystemPrompt, 8 8 buildConversation, 9 9 getUserCredentials, 10 10 incrementAndCheckDailyLimit, ··· 12 12 splitResponseIntoChunks, 13 13 processUrls, 14 14 } from '@/commands/utilities/ai'; 15 + 16 + function buildSystemPrompt( 17 + usingDefaultKey: boolean, 18 + client?: BotClient, 19 + model?: string, 20 + username?: string, 21 + interaction?: ChatInputCommandInteraction, 22 + isServer?: boolean, 23 + serverName?: string, 24 + ): string { 25 + const basePrompt = originalBuildSystemPrompt( 26 + usingDefaultKey, 27 + client, 28 + model, 29 + username, 30 + interaction, 31 + isServer, 32 + serverName, 33 + ); 34 + 35 + const reactionInstructions = ` 36 + **AVAILABLE TOOLS:** 37 + 38 + **REACTION TOOLS:** 39 + - {reaction:"๐Ÿ˜€"} - React to the user's message with a unicode emoji 40 + - {reaction:{"emoji":":thumbsup:"}} - React using a named emoji if available 41 + - {reaction:{"emoji":"<:name:123456789012345678>"}} - React with a custom emoji by ID (or animated <a:name:id>) 42 + 43 + **REACTION GUIDELINES:** 44 + - When asked to react, ALWAYS use the {reaction:"emoji"} tool call 45 + - Use reactions sparingly and only when it adds value to the conversation 46 + - Add at most 1โ€“2 reactions for a single message 47 + - Do not include the reaction tool call text in your visible reply 48 + - Common reactions: ๐Ÿ˜€ ๐Ÿ˜„ ๐Ÿ‘ ๐Ÿ‘Ž โค๏ธ ๐Ÿ”ฅ โญ ๐ŸŽ‰ ๐Ÿ‘ 49 + - Example: If asked to react with thumbs up, use {reaction:"๐Ÿ‘"} and respond normally 50 + - IMPORTANT: If you use a reaction tool, you MUST also provide a text response - never use ONLY a reaction tool 51 + - The reaction tool is for adding emoji reactions, not for replacing your response 52 + 53 + **NEW MESSAGE TOOL - CRITICAL GUIDELINES:** 54 + **WHAT IT DOES:** 55 + - {newmessage:} splits your response into multiple Discord messages 56 + - This simulates how real users send follow-up messages 57 + - Use it to break up long responses or create natural conversation flow 58 + 59 + **WHEN TO USE IT:** 60 + - Only when you have SUBSTANTIAL content to split (multiple paragraphs, distinct thoughts) 61 + - When your response is naturally long and would benefit from being split 62 + - DO NOT use it for short responses or single sentences 63 + 64 + **HOW TO USE IT CORRECTLY:** 65 + - Place it BETWEEN meaningful parts of your response 66 + - You MUST have content BEFORE and AFTER the tool 67 + - CORRECT: "Here's my first point about this topic. {newmessage:} And here's my second point that continues the thought." 68 + - CORRECT: "Let me explain this in parts. First, the background information. {newmessage:} Now, here's how it applies to your situation." 69 + 70 + **NEVER DO THESE - THEY ARE WRONG:** 71 + - WRONG: "{newmessage:} Here's my response" (starts with the tool) 72 + - WRONG: "Here's my response {newmessage:}" (ends with the tool) 73 + - WRONG: "{newmessage:}" (tool by itself with no content) 74 + - WRONG: Using it for responses under 200 characters 75 + - WRONG: Using it to split single sentences or short phrases 76 + 77 + **VALIDATION CHECK:** 78 + - Before using {newmessage:}, ask yourself: "Do I have meaningful content both before AND after this tool?" 79 + - If the answer is NO, don't use the tool 80 + - If your response is short, send it as one message 81 + - The tool should feel natural and conversational, not forced 82 + 83 + **EXAMPLES OF PROPER USAGE:** 84 + - "I've analyzed your code and found several issues. The first is a syntax error on line 23. {newmessage:} The second issue is a logical error in your loop condition that could cause an infinite loop." 85 + - "Let me break down the solution for you. Step 1: Understand the problem by identifying the root cause. {newmessage:} Step 2: Implement the fix by refactoring the problematic function. {newmessage:} Step 3: Test your solution thoroughly before deploying." 86 + 87 + **IMPORTANT DISTINCTION:** 88 + - \`{newmessage:}\` is a FORMATTING TOOL - it just splits your response into multiple messages 89 + - \`{newmessage:}\` does NOT execute any real functionality like other tools 90 + - You can use \`{newmessage:}\` without any other tool calls - it's just for message formatting 91 + - Don't expect any feedback or results from \`{newmessage:}\` - it just creates a new message 92 + 93 + **AVOIDING TOOL LOOPS:** 94 + - If you find yourself repeatedly trying to use tools but generating no content, STOP and respond normally 95 + - If your tool usage isn't working as expected, provide a simple text response instead 96 + - Never let tool usage prevent you from giving a helpful response 97 + - When in doubt, respond with plain text rather than complex tool combinations 98 + `; 99 + 100 + return basePrompt + reactionInstructions; 101 + } 102 + 103 + import { extractToolCalls as extractSlashToolCalls } from '@/utils/commandExecutor'; 104 + import _fetch from '@/utils/dynamicFetch'; 105 + import { executeMessageToolCall, type MessageToolCall } from '@/utils/messageToolExecutor'; 15 106 import type { ConversationMessage, AIResponse } from '@/commands/utilities/ai'; 107 + import pool from '@/utils/pgClient'; 16 108 17 109 type ApiConfiguration = ReturnType<typeof getApiConfiguration>; 18 110 import { createMemoryManager } from '@/utils/memoryManager'; 19 111 20 112 const serverConversations = createMemoryManager<string, ConversationMessage[]>({ 21 - maxSize: 1000, 113 + maxSize: 5000, 22 114 maxAge: 2 * 60 * 60 * 1000, 23 115 cleanupInterval: 10 * 60 * 1000, 24 116 }); ··· 31 123 timestamp: number; 32 124 }> 33 125 >({ 34 - maxSize: 1000, 126 + maxSize: 5000, 35 127 maxAge: 2 * 60 * 60 * 1000, 36 128 cleanupInterval: 10 * 60 * 1000, 37 129 }); ··· 41 133 maxAge: 2 * 60 * 60 * 1000, 42 134 cleanupInterval: 10 * 60 * 1000, 43 135 }); 136 + 137 + function extractMessageToolCalls(content: string): { 138 + cleanContent: string; 139 + toolCalls: MessageToolCall[]; 140 + } { 141 + const { cleanContent, toolCalls } = extractSlashToolCalls(content); 142 + 143 + return { cleanContent, toolCalls }; 144 + } 44 145 45 146 function getServerConversationKey(guildId: string): string { 46 147 return `server:${guildId}`; ··· 114 215 model: userCustomModel, 115 216 apiKey: userApiKey, 116 217 apiUrl: userApiUrl, 117 - } = await getUserCredentials(`user:${message.author.id}`); 218 + } = await getUserCredentials(message.author.id); 118 219 119 220 selectedModel = hasImages 120 221 ? 'google/gemma-3-4b-it' ··· 123 224 config = getApiConfiguration(userApiKey ?? null, selectedModel, userApiUrl ?? null); 124 225 usingDefaultKey = config.usingDefaultKey; 125 226 } else { 126 - selectedModel = hasImages ? 'google/gemma-3-4b-it' : 'google/gemini-2.5-flash-lite'; 227 + selectedModel = hasImages ? 'google/gemma-3-4b-it' : 'moonshotai/kimi-k2'; 127 228 128 229 config = getApiConfiguration(null, selectedModel, null); 230 + if (config.usingDefaultKey && !config.finalApiKey) { 231 + await message.reply({ 232 + content: 233 + 'โŒ AI is not configured. Please set OPENROUTER_API_KEY on the bot, or use `/ai` with your own API key.', 234 + allowedMentions: { parse: ['users'] as const }, 235 + }); 236 + return; 237 + } 129 238 } 130 239 131 240 logger.info( ··· 134 243 }`, 135 244 ); 136 245 246 + let replyContext = ''; 247 + if (message.reference?.messageId) { 248 + try { 249 + const repliedTo = await message.channel.messages.fetch(message.reference.messageId); 250 + if (repliedTo) { 251 + replyContext = `[Replying to ${repliedTo.author.username}: ${repliedTo.content}]\n\n`; 252 + } 253 + } catch (error) { 254 + logger.debug('Error fetching replied message:', error); 255 + } 256 + } 257 + 137 258 const systemPrompt = buildSystemPrompt( 138 259 usingDefaultKey, 139 260 this.client, ··· 144 265 !isDM ? message.guild?.name : undefined, 145 266 ); 146 267 268 + const baseContent = isDM ? message.content : message.content.replace(/<@!?\d+>/g, '').trim(); 269 + const messageWithContext = replyContext ? `${replyContext}${baseContent}` : baseContent; 270 + 147 271 let messageContent: 148 272 | string 149 273 | Array<{ ··· 153 277 url: string; 154 278 detail?: 'low' | 'high' | 'auto'; 155 279 }; 156 - }> = isDM ? message.content : message.content.replace(/<@!?\d+>/g, '').trim(); 280 + }>; 157 281 158 282 if (hasImages) { 159 283 const imageAttachments = message.attachments.filter( ··· 171 295 }; 172 296 }> = []; 173 297 174 - const cleanContent = isDM 175 - ? message.content 176 - : message.content.replace(/<@!?\d+>/g, '').trim(); 177 - if (cleanContent.trim()) { 298 + if (messageWithContext.trim()) { 178 299 contentArray.push({ 179 300 type: 'text', 180 - text: cleanContent, 301 + text: messageWithContext, 181 302 }); 182 303 } 183 304 ··· 192 313 }); 193 314 194 315 messageContent = contentArray; 316 + } else { 317 + messageContent = messageWithContext; 195 318 } 196 319 197 320 let conversation: ConversationMessage[] = []; ··· 244 367 245 368 const updatedConversation = buildConversation( 246 369 filteredConversation, 247 - messageContent, 370 + messageWithContext, 248 371 systemPrompt, 249 372 ); 250 373 251 - if (config.usingDefaultKey) { 252 - const exemptUserId = process.env.AI_EXEMPT_USER_ID; 253 - const actorId = message.author.id; 374 + const exemptUserId = process.env.AI_EXEMPT_USER_ID?.trim(); 375 + const actorId = message.author.id; 376 + const isExempt = actorId === exemptUserId; 377 + 378 + logger.debug( 379 + `AI limit check - usingDefaultKey: ${config.usingDefaultKey}, exemptUserId: ${exemptUserId}, actorId: ${actorId}, isExempt: ${isExempt}, isDM: ${isDM}`, 380 + ); 254 381 255 - if (actorId !== exemptUserId) { 256 - if (isDM) { 257 - const allowed = await incrementAndCheckDailyLimit(actorId, 10); 258 - if (!allowed) { 382 + if (config.usingDefaultKey && !isExempt) { 383 + if (isDM) { 384 + logger.debug(`Checking DM daily limit for user ${actorId}`); 385 + const allowed = await incrementAndCheckDailyLimit(actorId, 50); 386 + logger.debug(`DM daily limit check result for user ${actorId}: ${allowed}`); 387 + if (!allowed) { 388 + await message.reply( 389 + "โŒ You've reached your daily limit of 50 AI requests. " + 390 + 'Vote for Aethel to get more requests: https://top.gg/bot/1371031984230371369/vote\n' + 391 + 'Or set up your own API key using the `/ai` command.', 392 + ); 393 + return; 394 + } 395 + } else { 396 + let serverLimit = 30; 397 + try { 398 + const memberCount = message.guild?.memberCount || 0; 399 + if (memberCount >= 1000) { 400 + serverLimit = 500; 401 + } else if (memberCount >= 100) { 402 + serverLimit = 150; 403 + } 404 + 405 + const voteBonus = await pool.query( 406 + `SELECT COUNT(DISTINCT user_id) as voter_count 407 + FROM votes 408 + WHERE vote_timestamp > NOW() - INTERVAL '24 hours' 409 + AND user_id IN ( 410 + SELECT user_id FROM votes WHERE server_id IS NULL 411 + )`, 412 + ); 413 + 414 + const voterCount = parseInt(voteBonus.rows[0]?.voter_count || '0'); 415 + if (voterCount > 0) { 416 + const bonus = Math.min(voterCount * 20, 100); 417 + serverLimit += bonus; 418 + logger.debug( 419 + `Server ${message.guildId} vote bonus: +${bonus} (${voterCount} voters)`, 420 + ); 421 + } 422 + 423 + const serverAllowed = await incrementAndCheckServerDailyLimit( 424 + message.guildId!, 425 + serverLimit, 426 + ); 427 + if (!serverAllowed) { 259 428 await message.reply( 260 - "โŒ You've reached your daily limit of AI requests. Please try again tomorrow or set up your own API key using the `/ai` command.", 429 + `โŒ This server has reached its daily limit of ${serverLimit} AI requests. ` + 430 + `Vote for Aethel to get more requests: https://top.gg/bot/1371031984230371369/vote`, 261 431 ); 262 432 return; 263 433 } 264 - } else { 265 - let serverLimit = 30; 266 - try { 267 - const memberCount = message.guild?.memberCount || 0; 268 - if (memberCount >= 1000) { 269 - serverLimit = 500; 270 - } else if (memberCount >= 100) { 271 - serverLimit = 150; 272 - } 273 - 274 - const serverAllowed = await incrementAndCheckServerDailyLimit( 275 - message.guildId!, 276 - serverLimit, 434 + } catch (error) { 435 + logger.error('Error checking server member count:', error); 436 + const serverAllowed = await incrementAndCheckServerDailyLimit(message.guildId!, 30); 437 + if (!serverAllowed) { 438 + await message.reply( 439 + 'โŒ This server has reached its daily limit of AI requests. ' + 440 + 'Vote for Aethel to get more requests: https://top.gg/bot/1371031984230371369/vote', 277 441 ); 278 - if (!serverAllowed) { 279 - await message.reply( 280 - `โŒ This server has reached its daily limit of ${serverLimit} AI requests. Please try again tomorrow.`, 281 - ); 282 - return; 283 - } 284 - } catch (error) { 285 - logger.error('Error checking server member count:', error); 286 - const serverAllowed = await incrementAndCheckServerDailyLimit(message.guildId!, 30); 287 - if (!serverAllowed) { 288 - await message.reply( 289 - 'โŒ This server has reached its daily limit of AI requests. Please try again tomorrow.', 290 - ); 291 - return; 292 - } 442 + return; 293 443 } 294 444 } 295 445 } ··· 298 448 return; 299 449 } 300 450 301 - let aiResponse = await makeAIRequest(config, updatedConversation); 451 + const conversationWithTools = [...updatedConversation]; 452 + const executedResults: Array<{ type: string; payload: Record<string, unknown> }> = []; 453 + let aiResponse = await makeAIRequest(config, conversationWithTools); 302 454 303 455 if (!aiResponse && hasImages) { 304 456 logger.warn(`First attempt failed for ${selectedModel}, retrying once...`); 305 457 await new Promise((resolve) => setTimeout(resolve, 1000)); 306 - aiResponse = await makeAIRequest(config, updatedConversation); 458 + aiResponse = await makeAIRequest(config, conversationWithTools); 307 459 } 308 460 309 461 if (!aiResponse && hasImages) { 310 462 logger.warn(`Image model ${selectedModel} failed, falling back to text-only model`); 311 463 312 - let fallbackContent = message.content; 464 + let fallbackContent = messageWithContext; 313 465 if (Array.isArray(messageContent)) { 314 466 const textParts = messageContent 315 - .filter((item) => item.type === 'text') 316 - .map((item) => item.text) 317 - .filter((text) => text && text.trim()); 467 + .filter((item: { type: string; text?: string }) => item.type === 'text') 468 + .map((item: { type: string; text?: string }) => item.text) 469 + .filter((text: string | undefined) => text && text.trim()); 318 470 319 471 const imageParts = messageContent 320 - .filter((item) => item.type === 'image_url') 321 - .map((item) => `[Image: ${item.image_url?.url}]`); 472 + .filter( 473 + (item: { type: string; image_url?: { url: string } }) => item.type === 'image_url', 474 + ) 475 + .map( 476 + (item: { type: string; image_url?: { url: string } }) => 477 + `[Image: ${item.image_url?.url}]`, 478 + ); 322 479 323 480 fallbackContent = 324 481 [...textParts, ...imageParts].join(' ') || ··· 365 522 } 366 523 } 367 524 525 + const maxIterations = 3; 526 + let iteration = 0; 527 + let lastToolResponse = ''; 528 + let originalContentWithTools = aiResponse?.content || ''; 529 + 530 + while (aiResponse && iteration < maxIterations) { 531 + iteration++; 532 + const extraction = extractMessageToolCalls(aiResponse.content || ''); 533 + const toolCalls: MessageToolCall[] = extraction.toolCalls; 534 + 535 + const executableTools = toolCalls.filter((tc) => tc.name?.toLowerCase() !== 'newmessage'); 536 + const reactionTools = executableTools.filter((tc) => tc.name?.toLowerCase() === 'reaction'); 537 + const nonReactionTools = executableTools.filter( 538 + (tc) => tc.name?.toLowerCase() !== 'reaction', 539 + ); 540 + 541 + if (executableTools.length > 0 && nonReactionTools.length === 0) { 542 + logger.debug( 543 + `AI used only reactions (${reactionTools.length}), breaking loop and preserving tool calls`, 544 + ); 545 + originalContentWithTools = aiResponse.content || ''; 546 + aiResponse.content = extraction.cleanContent; 547 + break; 548 + } 549 + 550 + if (executableTools.length === 0) { 551 + aiResponse.content = extraction.cleanContent; 552 + break; 553 + } 554 + 555 + const currentToolResponse = JSON.stringify( 556 + executableTools.map((tc) => ({ name: tc.name, args: tc.args })), 557 + ); 558 + if (currentToolResponse === lastToolResponse) { 559 + logger.warn('AI stuck in tool loop, breaking out to prevent [NO CONTENT] responses'); 560 + aiResponse.content = 561 + extraction.cleanContent || 562 + 'I apologize, but I seem to be having trouble with the tools. Let me respond normally.'; 563 + break; 564 + } 565 + lastToolResponse = currentToolResponse; 566 + 567 + conversationWithTools.push({ role: 'assistant', content: aiResponse.content }); 568 + 569 + for (const tc of nonReactionTools) { 570 + const name = tc.name?.toLowerCase(); 571 + try { 572 + const result = await executeMessageToolCall(tc, message, this.client, { 573 + originalMessage: message, 574 + botMessage: undefined, 575 + }); 576 + const payload = { 577 + type: name, 578 + success: result.success, 579 + handled: result.handled, 580 + error: result.error || null, 581 + ...(result.result?.metadata || {}), 582 + }; 583 + 584 + executedResults.push({ type: name, payload }); 585 + conversationWithTools.push({ role: 'user', content: JSON.stringify(payload) }); 586 + 587 + logger.debug(`[MessageCreate] MCP tool ${name} executed:`, { 588 + success: result.success, 589 + handled: result.handled, 590 + }); 591 + } catch (e) { 592 + conversationWithTools.push({ role: 'user', content: `[Tool ${tc.name} error]` }); 593 + logger.error('[MessageCreate] Tool execution threw exception', { 594 + name: tc.name, 595 + error: (e as Error)?.message, 596 + }); 597 + } 598 + } 599 + 600 + aiResponse = await makeAIRequest(config, conversationWithTools); 601 + if (!aiResponse) break; 602 + 603 + const hasNewMessageTool = toolCalls.some((tc) => tc.name?.toLowerCase() === 'newmessage'); 604 + const cleanContent = extraction.cleanContent?.trim() || ''; 605 + 606 + if (hasNewMessageTool && !cleanContent && iteration >= 2) { 607 + logger.warn('AI stuck in newmessage misuse loop, forcing normal response'); 608 + aiResponse.content = 609 + 'I apologize for the confusion. Let me respond clearly without using any tools.'; 610 + break; 611 + } 612 + 613 + if (nonReactionTools.length === 0 && hasNewMessageTool) { 614 + logger.debug('Only newmessage formatting tools found, breaking iterative loop'); 615 + aiResponse.content = extraction.cleanContent; 616 + break; 617 + } 618 + } 619 + 368 620 if (!aiResponse) { 369 621 await message.reply({ 370 622 content: 'Sorry, I encountered an error processing your message. Please try again later.', ··· 373 625 return; 374 626 } 375 627 628 + if (!aiResponse.content || !aiResponse.content.trim()) { 629 + const last = executedResults[executedResults.length - 1]; 630 + if (last) { 631 + if ( 632 + (last.type === 'cat' || last.type === 'dog') && 633 + typeof last.payload.url === 'string' 634 + ) { 635 + aiResponse.content = `Here you go ${last.type === 'cat' ? '๐Ÿฑ' : '๐Ÿถ'}: ${last.payload.url}`; 636 + } else if (last.type === 'weather') { 637 + const p = last.payload as Record<string, string>; 638 + if (p.location && p.temperature) { 639 + aiResponse.content = `Weather for ${p.location}: ${p.temperature} (feels ${p.feels_like}), ${p.conditions}. Humidity ${p.humidity}, Wind ${p.wind_speed}, Pressure ${p.pressure}.`; 640 + } else if (p.description) { 641 + aiResponse.content = `Weather in ${p.location || 'the requested area'}: ${p.description}`; 642 + } 643 + } else if (last.type === 'wiki') { 644 + const p = last.payload as Record<string, string>; 645 + if (p.title || p.extract || p.url) { 646 + aiResponse.content = 647 + `${p.title || ''}\n${p.extract || ''}\n${p.url ? `<${p.url}>` : ''}`.trim(); 648 + } 649 + } 650 + } 651 + 652 + if (!aiResponse.content || !aiResponse.content.trim()) { 653 + logger.debug('AI response has no meaningful content, not sending message'); 654 + return; 655 + } 656 + } 657 + 376 658 aiResponse.content = processUrls(aiResponse.content); 377 659 aiResponse.content = aiResponse.content.replace(/@(everyone|here)/gi, '@\u200b$1'); 378 660 661 + const originalContent = originalContentWithTools || aiResponse.content || ''; 662 + const extraction = extractMessageToolCalls(originalContent); 663 + aiResponse.content = extraction.cleanContent; 664 + const toolCalls: MessageToolCall[] = extraction.toolCalls; 665 + const hasReactionTool = toolCalls.some((tc) => tc?.name?.toLowerCase() === 'reaction'); 666 + const originalCleaned = (extraction.cleanContent || '').trim(); 667 + 668 + logger.debug(`Final tool extraction: ${toolCalls.length} tools found`, { 669 + tools: toolCalls.map((tc) => tc.name), 670 + hasReaction: hasReactionTool, 671 + cleanContent: extraction.cleanContent?.substring(0, 50), 672 + }); 673 + 674 + if (!originalCleaned && hasReactionTool) { 675 + for (const tc of toolCalls) { 676 + if (!tc || !tc.name) continue; 677 + const name = tc.name.toLowerCase(); 678 + if (name !== 'reaction') continue; 679 + try { 680 + await executeMessageToolCall(tc, message, this.client, { 681 + originalMessage: message, 682 + botMessage: undefined, 683 + }); 684 + } catch (err) { 685 + logger.error('Error executing reaction tool on original message:', err); 686 + } 687 + } 688 + return; 689 + } 690 + 379 691 const { getUnallowedWordCategory } = await import('@/utils/validation'); 380 692 const category = getUnallowedWordCategory(aiResponse.content); 381 693 if (category) { ··· 388 700 return; 389 701 } 390 702 391 - await this.sendResponse(message, aiResponse); 703 + const cleaned = (aiResponse.content || '').trim(); 704 + const onlyReactions = !cleaned && hasReactionTool; 705 + if (onlyReactions) { 706 + const toolCallRegex = /{([^{}\s:]+):({[^{}]*}|[^{}]*)?}/g; 707 + const fallback = originalContent.replace(toolCallRegex, '').trim(); 708 + if (fallback) { 709 + aiResponse.content = fallback; 710 + } else { 711 + const textParts: string[] = []; 712 + for (const tc of toolCalls) { 713 + if (!tc || !tc.args) continue; 714 + const a = tc.args as Record<string, unknown>; 715 + const candidates = ['text', 'query', 'content', 'body', 'message']; 716 + for (const k of candidates) { 717 + const v = a[k] as unknown; 718 + if (typeof v === 'string' && v.trim()) { 719 + textParts.push(v.trim()); 720 + break; 721 + } 722 + } 723 + } 724 + if (textParts.length) { 725 + aiResponse.content = textParts.join(' '); 726 + } else { 727 + const reactionLabels: string[] = []; 728 + for (const tc of toolCalls) { 729 + if (!tc || !tc.name) continue; 730 + if (tc.name.toLowerCase() !== 'reaction') continue; 731 + const a = tc.args as Record<string, unknown>; 732 + const emojiCandidate = 733 + (a.emoji as string) || (a.query as string) || (a['emojiRaw'] as string) || ''; 734 + if (typeof emojiCandidate === 'string' && emojiCandidate.trim()) { 735 + reactionLabels.push(emojiCandidate.trim()); 736 + } 737 + } 738 + if (reactionLabels.length) { 739 + aiResponse.content = `Reacted with ${reactionLabels.join(', ')}`; 740 + } else { 741 + aiResponse.content = 'Reacted.'; 742 + } 743 + } 744 + } 745 + } 746 + 747 + const sent = await this.sendResponse(message, aiResponse, executedResults); 748 + const sentMessage: Message | undefined = sent as Message | undefined; 749 + 750 + if (extraction.toolCalls.length > 0) { 751 + const executed: Array<{ name: string; success: boolean }> = []; 752 + logger.debug( 753 + `[MessageCreate] Final execution - processing ${extraction.toolCalls.length} tool calls:`, 754 + extraction.toolCalls.map((tc) => ({ name: tc.name, args: tc.args })), 755 + ); 756 + for (const tc of extraction.toolCalls) { 757 + if (!tc || !tc.name) continue; 758 + const name = tc.name.toLowerCase(); 759 + if (name === 'reaction') { 760 + try { 761 + const result = await executeMessageToolCall(tc, message, this.client, { 762 + originalMessage: message, 763 + botMessage: sentMessage, 764 + }); 765 + executed.push({ name, success: !!result?.success }); 766 + } catch (err) { 767 + logger.error('Error executing message tool:', { name, err }); 768 + executed.push({ name, success: false }); 769 + } 770 + } else { 771 + const target = sentMessage || message; 772 + try { 773 + const result = await executeMessageToolCall(tc, target, this.client, { 774 + originalMessage: message, 775 + botMessage: sentMessage, 776 + }); 777 + 778 + if (name === 'cat' || name === 'dog') { 779 + const imageUrl = result.result?.metadata?.url as string; 780 + if (imageUrl && imageUrl.startsWith('http')) { 781 + await target.reply({ content: '', files: [imageUrl] }); 782 + } 783 + } else if (name === 'weather' || name === 'wiki') { 784 + const textContent = result.result?.content?.find((c) => c.type === 'text')?.text; 785 + if (textContent) { 786 + await target.reply(processUrls(textContent)); 787 + } 788 + } 789 + 790 + executed.push({ name, success: result.success }); 791 + } catch (err) { 792 + logger.error(`Error executing MCP tool ${name}:`, { err }); 793 + executed.push({ name, success: false }); 794 + } 795 + } 796 + } 797 + 798 + if (onlyReactions) { 799 + const anyFailed = executed.some((e) => e.name === 'reaction' && !e.success); 800 + if (anyFailed && sentMessage) { 801 + try { 802 + await sentMessage.edit( 803 + 'I tried to react, but I do not have permission to add reactions here or the emoji was invalid.', 804 + ); 805 + } catch (e) { 806 + logger.error('Failed to edit placeholder message after reaction failure:', e); 807 + } 808 + } 809 + } 810 + } 392 811 393 812 const userMessage: ConversationMessage = { 394 813 role: 'user', 395 - content: messageContent, 814 + content: messageWithContext, 396 815 username: message.author.username, 397 816 }; 398 817 const assistantMessage: ConversationMessage = { ··· 413 832 414 833 logger.info(`${isDM ? 'DM' : 'Server'} response sent successfully`); 415 834 } catch (error) { 416 - logger.error( 417 - `Error processing ${isDM ? 'DM' : 'server message'}:`, 418 - error instanceof Error ? error.message : String(error), 419 - ); 835 + const err = error as Error; 836 + logger.error(`Error processing ${isDM ? 'DM' : 'server message'}:`, { 837 + message: err?.message, 838 + stack: err?.stack, 839 + raw: error, 840 + }); 420 841 try { 421 842 await message.reply( 422 843 'Sorry, I encountered an error processing your message. Please try again later.', ··· 427 848 } 428 849 } 429 850 430 - private async sendResponse(message: Message, aiResponse: AIResponse): Promise<void> { 851 + private async sendResponse( 852 + message: Message, 853 + aiResponse: AIResponse, 854 + executedResults?: Array<{ type: string; payload: Record<string, unknown> }>, 855 + ): Promise<Message | void> { 431 856 let fullResponse = ''; 432 857 433 858 if (aiResponse.reasoning) { ··· 435 860 } 436 861 437 862 fullResponse += aiResponse.content; 863 + fullResponse = processUrls(fullResponse); 438 864 439 - const maxLength = 2000; 440 - if (fullResponse.length <= maxLength) { 441 - await message.reply({ 442 - content: fullResponse, 865 + const imageFiles: string[] = []; 866 + if (executedResults) { 867 + for (const result of executedResults) { 868 + if ( 869 + (result.type === 'cat' || result.type === 'dog') && 870 + result.payload.url && 871 + typeof result.payload.url === 'string' 872 + ) { 873 + imageFiles.push(result.payload.url); 874 + } 875 + } 876 + } 877 + 878 + if (!fullResponse || !fullResponse.trim()) { 879 + logger.debug('AI response has no meaningful content, not sending message'); 880 + return; 881 + } 882 + 883 + const newMessageOnlyRegex = /^\s*\{newmessage:\}\s*$/; 884 + if (newMessageOnlyRegex.test(fullResponse)) { 885 + logger.warn('AI misused newmessage tool - sent only {newmessage:} with no content'); 886 + return; 887 + } 888 + 889 + const newMessageRegex = /\{newmessage:\}/g; 890 + if (newMessageRegex.test(fullResponse)) { 891 + const parts = fullResponse.split(/\{newmessage:\}/); 892 + 893 + if (parts[0].trim() === '') { 894 + logger.warn('AI misused newmessage tool - started response with {newmessage:}'); 895 + parts.shift(); 896 + if (parts.length === 0) { 897 + return await message.reply({ 898 + content: fullResponse.replace(/\{newmessage:\}/g, '').trim() || '\u200b', 899 + allowedMentions: { parse: ['users'] as const }, 900 + }); 901 + } 902 + } 903 + 904 + const first = await message.reply({ 905 + content: parts[0].trim() || '\u200b', 906 + files: imageFiles.length > 0 ? imageFiles : undefined, 443 907 allowedMentions: { parse: ['users'] as const }, 444 908 }); 445 - } else { 446 - const chunks = splitResponseIntoChunks(fullResponse, maxLength); 447 909 448 - await message.reply({ content: chunks[0], allowedMentions: { parse: ['users'] as const } }); 910 + for (let i = 1; i < parts.length; i++) { 911 + if ('send' in message.channel && parts[i].trim()) { 912 + const delay = Math.floor(Math.random() * 900) + 300; 913 + await new Promise((resolve) => setTimeout(resolve, delay)); 449 914 450 - for (let i = 1; i < chunks.length; i++) { 451 - if ('send' in message.channel) { 452 915 await message.channel.send({ 453 - content: chunks[i], 916 + content: parts[i].trim(), 454 917 allowedMentions: { parse: ['users'] as const }, 455 918 }); 456 919 } 920 + } 921 + return first; 922 + } 923 + const conversationChunks = this.splitIntoConversationalChunks(fullResponse); 924 + 925 + const first = await message.reply({ 926 + content: conversationChunks[0], 927 + files: imageFiles.length > 0 ? imageFiles : undefined, 928 + allowedMentions: { parse: ['users'] as const }, 929 + }); 930 + 931 + for (let i = 1; i < conversationChunks.length; i++) { 932 + if ('send' in message.channel) { 933 + const delay = Math.floor(Math.random() * 900) + 300; 934 + await new Promise((resolve) => setTimeout(resolve, delay)); 935 + 936 + await message.channel.send({ 937 + content: conversationChunks[i], 938 + allowedMentions: { parse: ['users'] as const }, 939 + }); 457 940 } 458 941 } 942 + return first; 943 + } 944 + 945 + private splitIntoConversationalChunks(text: string): string[] { 946 + if (!text || text.length <= 200) { 947 + return [text]; 948 + } 949 + 950 + const chunks: string[] = []; 951 + const paragraphs = text.split(/\n\n+/); 952 + 953 + for (const paragraph of paragraphs) { 954 + if (paragraph.length < 200) { 955 + chunks.push(paragraph); 956 + } else { 957 + const sentences = paragraph.split(/(?<=[.!?])\s+/); 958 + 959 + let currentChunk = ''; 960 + for (const sentence of sentences) { 961 + if (currentChunk.length + sentence.length > 200 && currentChunk.length > 0) { 962 + chunks.push(currentChunk); 963 + currentChunk = sentence; 964 + } else { 965 + if (currentChunk && !currentChunk.endsWith('\n')) { 966 + currentChunk += ' '; 967 + } 968 + currentChunk += sentence; 969 + } 970 + 971 + const hasEndPunctuation = /[.!?]$/.test(sentence); 972 + const breakChance = hasEndPunctuation ? 0.7 : 0.3; 973 + 974 + if (currentChunk.length > 100 && Math.random() < breakChance) { 975 + chunks.push(currentChunk); 976 + currentChunk = ''; 977 + } 978 + } 979 + 980 + if (currentChunk) { 981 + chunks.push(currentChunk); 982 + } 983 + } 984 + } 985 + 986 + const fillerMessages = ['hmm', 'let me think', 'one sec', 'actually', 'wait', 'so basically']; 987 + 988 + if (chunks.length > 1 && Math.random() < 0.3) { 989 + const position = Math.floor(Math.random() * (chunks.length - 1)) + 1; 990 + const filler = fillerMessages[Math.floor(Math.random() * fillerMessages.length)]; 991 + chunks.splice(position, 0, filler); 992 + } 993 + 994 + const maxLength = 2000; 995 + const finalChunks: string[] = []; 996 + 997 + for (const chunk of chunks) { 998 + if (chunk.length <= maxLength) { 999 + finalChunks.push(chunk); 1000 + } else { 1001 + finalChunks.push(...splitResponseIntoChunks(chunk, maxLength)); 1002 + } 1003 + } 1004 + 1005 + return finalChunks; 459 1006 } 460 1007 }
+11 -2
src/events/ready.ts
··· 3 3 import { loadActiveReminders } from '@/commands/utilities/remind'; 4 4 5 5 export default class ReadyEvent { 6 - constructor(c: BotClient) { 7 - c.once('ready', () => this.readyEvent(c)); 6 + private startTime: number; 7 + 8 + constructor(c: BotClient, startTime: number = Date.now()) { 9 + this.startTime = startTime; 10 + c.once('clientReady', () => this.readyEvent(c)); 8 11 } 9 12 10 13 private async readyEvent(client: BotClient) { 11 14 try { 12 15 logger.info(`Logged in as ${client.user?.username}`); 16 + 13 17 await client.application?.commands.fetch({ withLocalizations: true }); 14 18 15 19 await loadActiveReminders(client); 20 + 21 + const { sendDeploymentNotification } = await import('../utils/sendDeploymentNotification.js'); 22 + await sendDeploymentNotification(this.startTime); 23 + 24 + logger.info('Bot fully initialized and ready'); 16 25 } catch (error) { 17 26 logger.error('Error during ready event:', error); 18 27 }
+1 -1
src/handlers/initialzeCommands.ts
··· 33 33 await import(commandUrl) 34 34 ).default) as SlashCommandProps | RemindCommandProps; 35 35 if (!command.data) { 36 - console.log('No command data in file', `${cat}/${file}.. Skipping`); 36 + logger.warn('No command data in file', `${cat}/${file}.. Skipping`); 37 37 continue; 38 38 } 39 39 command.category = cat;
+27 -9
src/index.ts
··· 2 2 import e from 'express'; 3 3 import helmet from 'helmet'; 4 4 import cors from 'cors'; 5 - 5 + import path from 'path'; 6 + import { fileURLToPath } from 'url'; 6 7 import BotClient from './services/Client'; 7 8 import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './config'; 8 9 import rateLimit from 'express-rate-limit'; 9 10 import authenticateApiKey from './middlewares/verifyApiKey'; 11 + import { authenticateToken } from './middlewares/auth'; 10 12 import status from './routes/status'; 11 13 import authRoutes from './routes/auth'; 12 14 import todosRoutes from './routes/todos'; 13 15 import apiKeysRoutes from './routes/apiKeys'; 14 16 import remindersRoutes from './routes/reminders'; 17 + import voteWebhookRoutes from './routes/voteWebhook'; 15 18 import { resetOldStrikes } from './utils/userStrikes'; 16 19 import logger from './utils/logger'; 17 20 ··· 28 31 29 32 const app = e(); 30 33 const startTime = Date.now(); 34 + const __filename = fileURLToPath(import.meta.url); 35 + const __dirname = path.dirname(__filename); 36 + const distPath = path.resolve(__dirname, '../web/dist'); 31 37 32 38 app.use(helmet()); 33 39 app.use( ··· 75 81 76 82 const bot = new BotClient(); 77 83 bot.init(); 84 + 85 + app.use(async (req, res, next) => { 86 + const start = process.hrtime.bigint(); 87 + res.on('finish', () => { 88 + const durMs = Number(process.hrtime.bigint() - start) / 1e6; 89 + const safePath = req.baseUrl ? `${req.baseUrl}${req.path}` : req.path; 90 + logger.debug(`API [${req.method}] ${safePath} ${res.statusCode} ${durMs.toFixed(1)}ms`); 91 + }); 92 + next(); 93 + }); 78 94 79 95 app.use('/api/auth', authRoutes); 80 96 app.use('/api/todos', todosRoutes); 81 97 app.use('/api/user/api-keys', apiKeysRoutes); 82 - app.use('/api/reminders', remindersRoutes); 98 + app.use('/api/reminders', authenticateToken, remindersRoutes); 99 + app.use('/api', voteWebhookRoutes); 83 100 84 101 app.use('/api/status', authenticateApiKey, status(bot)); 85 102 ··· 87 104 res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); 88 105 }); 89 106 90 - app.use(e.static('web/dist')); 107 + app.use(e.static(distPath, { index: false, maxAge: '1h' })); 91 108 92 - app.get('*', (req, res) => { 93 - if (req.path.startsWith('/api/')) { 94 - return res.status(404).json({ error: 'Not found' }); 95 - } 96 - res.sendFile('index.html', { root: 'web/dist' }); 109 + app.get(/^\/(?!api\/).*/, (req, res) => { 110 + return res.sendFile(path.join(distPath, 'index.html')); 111 + }); 112 + 113 + app.use((req, res) => { 114 + return res.status(404).json({ status: 404, message: 'Not Found' }); 97 115 }); 98 116 99 117 setInterval( ··· 106 124 const server = app.listen(PORT, async () => { 107 125 logger.debug('Aethel is live on', `http://localhost:${PORT}`); 108 126 109 - const { sendDeploymentNotification } = await import('./utils/sendDeploymentNotification'); 127 + const { sendDeploymentNotification } = await import('./utils/sendDeploymentNotification.js'); 110 128 await sendDeploymentNotification(startTime); 111 129 }); 112 130
+6 -20
src/middlewares/auth.ts
··· 2 2 import jwt from 'jsonwebtoken'; 3 3 import logger from '../utils/logger'; 4 4 5 - const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; 5 + if (!process.env.JWT_SECRET) { 6 + throw new Error('JWT_SECRET environment variable is required'); 7 + } 8 + 9 + const JWT_SECRET = process.env.JWT_SECRET; 6 10 7 11 interface JwtPayload { 8 12 userId: string; ··· 22 26 } 23 27 24 28 try { 25 - const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; 29 + const decoded = jwt.verify(token, JWT_SECRET) as unknown as JwtPayload; 26 30 req.user = decoded; 27 31 next(); 28 32 } catch (error) { ··· 37 41 return res.status(500).json({ error: 'Token verification failed' }); 38 42 } 39 43 }; 40 - 41 - export const optionalAuth = (req: Request, res: Response, next: NextFunction) => { 42 - const authHeader = req.headers['authorization']; 43 - const token = authHeader && authHeader.split(' ')[1]; 44 - 45 - if (!token) { 46 - return next(); 47 - } 48 - 49 - try { 50 - const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; 51 - req.user = decoded; 52 - } catch (error) { 53 - logger.debug('Optional auth token verification failed:', error); 54 - } 55 - 56 - next(); 57 - };
+1 -1
src/middlewares/verifyApiKey.ts
··· 1 1 import * as config from '@/config'; 2 2 import { RequestHandler } from 'express'; 3 3 4 - const authenticateApiKey: RequestHandler = (req, res, next) => { 4 + export const authenticateApiKey: RequestHandler = (req, res, next) => { 5 5 const apiKey = req.headers['x-api-key']; 6 6 if (!apiKey || typeof apiKey !== 'string') { 7 7 res.status(401).json({ error: 'Unauthorized: Missing API key' });
+120 -26
src/routes/apiKeys.ts
··· 1 1 import { Router } from 'express'; 2 + import axios from 'axios'; 2 3 import pool from '../utils/pgClient'; 3 4 import logger from '../utils/logger'; 4 5 import { authenticateToken } from '../middlewares/auth'; ··· 11 12 'openrouter.ai', 12 13 'generativelanguage.googleapis.com', 13 14 'api.anthropic.com', 15 + 'api.mistral.ai', 16 + 'api.deepseek.com', 17 + 'api.together.xyz', 18 + 'api.perplexity.ai', 19 + 'api.groq.com', 20 + 'api.lepton.ai', 21 + 'api.deepinfra.com', 22 + 'api.moonshot.ai', 23 + 'api.x.ai', 14 24 ]; 15 25 16 26 function getOpenAIClient(apiKey: string, baseURL?: string): OpenAI { ··· 250 260 }); 251 261 } 252 262 253 - const testModel = model || 'openai/gpt-4o-mini'; 254 - const client = getOpenAIClient(apiKey, fullApiUrl); 263 + const isGemini = parsedUrl.hostname === 'generativelanguage.googleapis.com'; 264 + 265 + if (isGemini) { 266 + const listModelsUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`; 267 + 268 + try { 269 + const listResponse = await axios.get(listModelsUrl, { 270 + headers: { 'Content-Type': 'application/json' }, 271 + timeout: 10000, 272 + }); 255 273 256 - try { 257 - const response = await client.chat.completions.create({ 258 - model: testModel, 259 - messages: [ 274 + interface ModelInfo { 275 + name: string; 276 + supportedGenerationMethods?: string[]; 277 + [key: string]: unknown; 278 + } 279 + 280 + const availableModels: ModelInfo[] = listResponse.data?.models || []; 281 + const workingModel = availableModels.find((m) => 282 + m.supportedGenerationMethods?.includes('generateContent'), 283 + ); 284 + 285 + if (!workingModel) { 286 + throw new Error('No models found that support generateContent'); 287 + } 288 + 289 + const testPrompt = 290 + 'Hello! This is a test message. Please respond with "API key test successful!"'; 291 + const generateUrl = `https://generativelanguage.googleapis.com/v1beta/${workingModel.name}:generateContent?key=${apiKey}`; 292 + 293 + const response = await axios.post( 294 + generateUrl, 260 295 { 261 - role: 'user', 262 - content: 263 - 'Hello! This is a test message. Please respond with "API key test successful!"', 296 + contents: [ 297 + { 298 + role: 'user', 299 + parts: [ 300 + { 301 + text: testPrompt, 302 + }, 303 + ], 304 + }, 305 + ], 264 306 }, 265 - ], 266 - max_tokens: 50, 267 - temperature: 0.1, 268 - }); 307 + { 308 + headers: { 'Content-Type': 'application/json' }, 309 + timeout: 10000, 310 + }, 311 + ); 269 312 270 - const testMessage = response.choices?.[0]?.message?.content || 'Test completed'; 313 + const testMessage = 314 + response.data?.candidates?.[0]?.content?.parts?.[0]?.text || 'Test completed'; 271 315 272 - logger.info(`API key test successful for user ${userId}`); 273 - res.json({ 274 - success: true, 275 - message: 'API key is valid and working!', 276 - testResponse: testMessage, 277 - }); 278 - } catch (error: unknown) { 279 - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 280 - logger.warn(`API key test failed for user ${userId}: ${errorMessage}`); 281 - return res.status(400).json({ 282 - error: `API key test failed: ${errorMessage}`, 283 - }); 316 + logger.info( 317 + `Gemini API key test successful for user ${userId} using model ${workingModel.name}`, 318 + ); 319 + return res.json({ 320 + success: true, 321 + message: 'Gemini API key is valid and working!', 322 + testResponse: testMessage, 323 + model: workingModel.name.split('/').pop(), 324 + }); 325 + } catch (error: unknown) { 326 + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 327 + const response = 328 + error && typeof error === 'object' && 'response' in error 329 + ? (error as { response?: { status?: number; data?: unknown } }).response 330 + : undefined; 331 + 332 + logger.warn(`Gemini API key test failed for user ${userId}:`, { 333 + error: errorMessage, 334 + status: response?.status, 335 + data: response?.data, 336 + }); 337 + return res.status(400).json({ 338 + error: `Gemini API key test failed: ${errorMessage}`, 339 + details: 340 + response?.data && typeof response.data === 'object' && response.data !== null 341 + ? (response.data as { error?: { details?: unknown } }).error?.details 342 + : undefined, 343 + }); 344 + } 345 + } else { 346 + const testModel = model || 'gpt-5-nano'; 347 + const client = getOpenAIClient(apiKey, fullApiUrl); 348 + 349 + try { 350 + const response = await client.chat.completions.create({ 351 + model: testModel, 352 + messages: [ 353 + { 354 + role: 'user', 355 + content: 356 + 'Hello! This is a test message. Please respond with "API key test successful!"', 357 + }, 358 + ], 359 + max_tokens: 50, 360 + temperature: 0.1, 361 + }); 362 + 363 + const testMessage = response.choices?.[0]?.message?.content || 'Test completed'; 364 + 365 + logger.info(`API key test successful for user ${userId}`); 366 + return res.json({ 367 + success: true, 368 + message: 'API key is valid and working!', 369 + testResponse: testMessage, 370 + }); 371 + } catch (error: unknown) { 372 + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 373 + logger.warn(`API key test failed for user ${userId}: ${errorMessage}`); 374 + return res.status(400).json({ 375 + error: `API key test failed: ${errorMessage}`, 376 + }); 377 + } 284 378 } 285 379 } catch (error) { 286 380 logger.error('Error testing API key:', error);
+6 -7
src/routes/auth.ts
··· 6 6 7 7 const router = Router(); 8 8 9 - const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; 10 - const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET; 11 - const DISCORD_REDIRECT_URI = 12 - process.env.DISCORD_REDIRECT_URI || 'http://localhost:8080/api/auth/discord/callback'; 9 + const DISCORD_CLIENT_ID = process.env.CLIENT_ID; 10 + const DISCORD_CLIENT_SECRET = process.env.CLIENT_SECRET; 11 + const DISCORD_REDIRECT_URI = process.env.REDIRECT_URI; 13 12 const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; 14 - const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:2020'; 13 + const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000'; 15 14 16 15 interface DiscordUser { 17 16 id: string; ··· 22 21 } 23 22 24 23 router.get('/discord', (req, res) => { 25 - const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(DISCORD_REDIRECT_URI)}&response_type=code&scope=identify`; 24 + const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(DISCORD_REDIRECT_URI!)}&response_type=code&scope=identify`; 26 25 res.redirect(discordAuthUrl); 27 26 }); 28 27 ··· 50 49 client_secret: DISCORD_CLIENT_SECRET!, 51 50 grant_type: 'authorization_code', 52 51 code: code as string, 53 - redirect_uri: DISCORD_REDIRECT_URI, 52 + redirect_uri: DISCORD_REDIRECT_URI!, 54 53 }), 55 54 }); 56 55
+96
src/routes/voteWebhook.ts
··· 1 + import { Router } from 'express'; 2 + import { recordVote } from '../utils/voteManager'; 3 + import logger from '../utils/logger'; 4 + 5 + const router = Router(); 6 + 7 + interface TopGGWebhookPayload { 8 + bot: string; 9 + user: string; 10 + type: 'upvote' | 'test'; 11 + isWeekend?: boolean; 12 + query?: string; 13 + } 14 + 15 + router.get('/webhooks/topgg', (_, res) => { 16 + return res.status(200).json({ status: 'ok', message: 'Webhook endpoint is active' }); 17 + }); 18 + router.post('/webhooks/topgg', async (req, res) => { 19 + const authHeader = req.headers.authorization; 20 + 21 + if (!authHeader || authHeader !== process.env.TOPGG_WEBHOOK_AUTH) { 22 + logger.warn('Unauthorized webhook attempt', { 23 + ip: req.ip, 24 + headers: req.headers, 25 + timestamp: new Date().toISOString(), 26 + }); 27 + return res.status(401).json({ error: 'Unauthorized' }); 28 + } 29 + 30 + try { 31 + const payload = req.body as TopGGWebhookPayload; 32 + 33 + logger.info('Received Top.gg webhook', { 34 + type: payload.type, 35 + userId: payload.user, 36 + botId: payload.bot, 37 + isWeekend: payload.isWeekend || false, 38 + query: payload.query, 39 + ip: req.ip, 40 + timestamp: new Date().toISOString(), 41 + }); 42 + 43 + if (payload.type !== 'test' && payload.type !== 'upvote') { 44 + logger.warn('Received unknown webhook type', { type: payload.type }); 45 + return res.status(400).json({ success: false, message: 'Invalid webhook type' }); 46 + } 47 + 48 + const userId = payload.user; 49 + const isTest = payload.type === 'test'; 50 + 51 + logger.info(`Processing ${isTest ? 'test ' : ''}vote`, { 52 + userId, 53 + isTest, 54 + isWeekend: payload.isWeekend || false, 55 + }); 56 + 57 + const result = await recordVote(userId); 58 + 59 + logger.info('Processed vote', { 60 + userId, 61 + creditsAwarded: result.creditsAwarded, 62 + nextVote: result.nextVoteAvailable.toISOString(), 63 + isWeekend: payload.isWeekend || false, 64 + }); 65 + 66 + if (!result.success) { 67 + return res.status(200).json({ 68 + success: false, 69 + message: 'Vote already processed', 70 + nextVote: result.nextVoteAvailable.toISOString(), 71 + }); 72 + } 73 + 74 + return res.status(200).json({ 75 + success: true, 76 + message: 'Vote processed successfully', 77 + creditsAwarded: result.creditsAwarded, 78 + isWeekend: payload.isWeekend || false, 79 + }); 80 + } catch (error: unknown) { 81 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 82 + const errorStack = error instanceof Error ? error.stack : undefined; 83 + 84 + logger.error('Error processing webhook:', { 85 + error: errorMessage, 86 + stack: errorStack, 87 + headers: req.headers, 88 + body: req.body, 89 + ip: req.ip, 90 + timestamp: new Date().toISOString(), 91 + }); 92 + return res.status(500).json({ success: false, message: 'Internal server error' }); 93 + } 94 + }); 95 + 96 + export default router;
+41 -3
src/services/Client.ts
··· 21 21 // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 22 public t = new Collection<string, any>(); 23 23 public socialMediaManager?: SocialMediaManager; 24 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 + public lastModalRawByUser: Map<string, any> = new Map(); 24 26 25 27 constructor() { 26 28 super({ ··· 41 43 }, 42 44 }); 43 45 BotClient.instance = this; 46 + 47 + interface RawPacket { 48 + t: string; 49 + d?: { 50 + type: number; 51 + member?: { 52 + user: { 53 + id: string; 54 + }; 55 + }; 56 + user?: { 57 + id: string; 58 + }; 59 + }; 60 + } 61 + 62 + this.on('raw', (packet: RawPacket) => { 63 + try { 64 + if (packet?.t !== 'INTERACTION_CREATE') return; 65 + const d = packet.d; 66 + if (d?.type !== 5) return; 67 + const userId = d?.member?.user?.id || d?.user?.id; 68 + if (!userId) return; 69 + this.lastModalRawByUser.set(userId, d); 70 + setTimeout( 71 + () => { 72 + if (this.lastModalRawByUser.get(userId) === d) { 73 + this.lastModalRawByUser.delete(userId); 74 + } 75 + }, 76 + 5 * 60 * 1000, 77 + ); 78 + } catch (e) { 79 + logger.warn('Failed to capture raw modal payload', e); 80 + } 81 + }); 44 82 } 45 83 46 84 public static getInstance(): BotClient | null { ··· 56 94 } 57 95 58 96 private async setupEvents() { 59 - console.log('Initializing events...'); 97 + logger.info('Initializing events...'); 60 98 const eventsDir = path.join(srcDir, 'events'); 61 99 for (const event of readdirSync(path.join(eventsDir))) { 62 100 const filepath = path.join(eventsDir, event); ··· 127 165 128 166 const shutdown = async (signal?: NodeJS.Signals) => { 129 167 try { 130 - console.log(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); 168 + logger.info(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); 131 169 await this.socialMediaManager?.cleanup(); 132 170 await pool.end(); 133 - console.log('Database pool closed. Exiting.'); 171 + logger.info('Database pool closed. Exiting.'); 134 172 } catch (e) { 135 173 console.error('Error during graceful shutdown:', e); 136 174 } finally {
+278
src/services/massive.ts
··· 1 + import { 2 + DefaultApi, 3 + GetStocksAggregatesSortEnum, 4 + GetStocksAggregatesTimespanEnum, 5 + GetStocksSnapshotTicker200Response, 6 + GetStocksSnapshotTicker200ResponseAllOfTicker, 7 + GetTicker200ResponseResults, 8 + GetStocksAggregates200Response, 9 + ListTickers200ResponseResultsInner, 10 + ListTickersMarketEnum, 11 + ListTickersOrderEnum, 12 + ListTickersSortEnum, 13 + restClient, 14 + } from '@massive.com/client-js'; 15 + import * as config from '@/config'; 16 + import logger from '@/utils/logger'; 17 + import { createRateLimiter } from '@/utils/rateLimiter'; 18 + import type { AxiosError } from 'axios'; 19 + 20 + const MASSIVE_RATE_LIMIT = 45; 21 + const rateLimiter = createRateLimiter(MASSIVE_RATE_LIMIT); 22 + 23 + let cachedClient: DefaultApi | null = null; 24 + 25 + export type StockTimeframe = '1d' | '5d' | '1m' | '3m' | '1y'; 26 + 27 + interface TimeframeConfig { 28 + multiplier: number; 29 + timespan: GetStocksAggregatesTimespanEnum; 30 + daysBack: number; 31 + limit: number; 32 + displayWindowMs?: number; 33 + } 34 + 35 + const TIMEFRAME_CONFIG: Record<StockTimeframe, TimeframeConfig> = { 36 + '1d': { 37 + multiplier: 5, 38 + timespan: GetStocksAggregatesTimespanEnum.Minute, 39 + daysBack: 3, 40 + limit: 400, 41 + displayWindowMs: 36 * 60 * 60 * 1000, 42 + }, 43 + '5d': { 44 + multiplier: 15, 45 + timespan: GetStocksAggregatesTimespanEnum.Minute, 46 + daysBack: 7, 47 + limit: 500, 48 + displayWindowMs: 7 * 24 * 60 * 60 * 1000, 49 + }, 50 + '1m': { 51 + multiplier: 1, 52 + timespan: GetStocksAggregatesTimespanEnum.Day, 53 + daysBack: 40, 54 + limit: 120, 55 + }, 56 + '3m': { 57 + multiplier: 1, 58 + timespan: GetStocksAggregatesTimespanEnum.Day, 59 + daysBack: 110, 60 + limit: 200, 61 + }, 62 + '1y': { 63 + multiplier: 1, 64 + timespan: GetStocksAggregatesTimespanEnum.Week, 65 + daysBack: 400, 66 + limit: 400, 67 + }, 68 + }; 69 + 70 + const DAY_MS = 24 * 60 * 60 * 1000; 71 + 72 + export interface StockAggregatePoint { 73 + timestamp: number; 74 + open: number; 75 + high: number; 76 + low: number; 77 + close: number; 78 + volume: number; 79 + vwap?: number; 80 + } 81 + 82 + export interface StockOverview { 83 + detail?: GetTicker200ResponseResults; 84 + snapshot?: GetStocksSnapshotTicker200ResponseAllOfTicker; 85 + } 86 + 87 + function ensureClient(): DefaultApi { 88 + if (!config.MASSIVE_API_KEY) { 89 + throw new Error('Massive.com API key is not configured'); 90 + } 91 + 92 + if (!cachedClient) { 93 + cachedClient = restClient(config.MASSIVE_API_KEY, config.MASSIVE_API_BASE_URL, { 94 + pagination: false, 95 + }); 96 + } 97 + 98 + return cachedClient; 99 + } 100 + 101 + async function withClient<T>(callback: (client: DefaultApi) => Promise<T>): Promise<T> { 102 + const client = ensureClient(); 103 + return rateLimiter.schedule(() => callback(client)); 104 + } 105 + 106 + function isNotFoundError(error: unknown): boolean { 107 + return ( 108 + typeof error === 'object' && 109 + error !== null && 110 + 'isAxiosError' in error && 111 + (error as AxiosError).response?.status === 404 112 + ); 113 + } 114 + 115 + export async function searchTickers( 116 + query: string, 117 + limit = 5, 118 + ): Promise<ListTickers200ResponseResultsInner[]> { 119 + if (!query.trim()) { 120 + return []; 121 + } 122 + 123 + const response = await withClient((client) => 124 + client.listTickers( 125 + undefined, 126 + undefined, 127 + ListTickersMarketEnum.Stocks, 128 + undefined, 129 + undefined, 130 + undefined, 131 + undefined, 132 + query, 133 + true, 134 + undefined, 135 + undefined, 136 + undefined, 137 + undefined, 138 + ListTickersOrderEnum.Asc, 139 + limit, 140 + ListTickersSortEnum.Ticker, 141 + ), 142 + ); 143 + 144 + return response.results ?? []; 145 + } 146 + 147 + export async function getTickerDetails( 148 + ticker: string, 149 + ): Promise<GetTicker200ResponseResults | null> { 150 + const normalized = ticker.trim().toUpperCase(); 151 + try { 152 + const response = await withClient((client) => client.getTicker(normalized)); 153 + return response.results ?? null; 154 + } catch (error) { 155 + if (isNotFoundError(error)) { 156 + return null; 157 + } 158 + throw error; 159 + } 160 + } 161 + 162 + export async function getTickerSnapshot( 163 + ticker: string, 164 + ): Promise<GetStocksSnapshotTicker200ResponseAllOfTicker | undefined> { 165 + const normalized = ticker.trim().toUpperCase(); 166 + try { 167 + const response: GetStocksSnapshotTicker200Response = await withClient((client) => 168 + client.getStocksSnapshotTicker(normalized), 169 + ); 170 + return response.ticker; 171 + } catch (error) { 172 + if (isNotFoundError(error)) { 173 + return undefined; 174 + } 175 + throw error; 176 + } 177 + } 178 + 179 + export async function getTickerOverview(ticker: string): Promise<StockOverview> { 180 + const [detail, snapshot] = await Promise.all([ 181 + getTickerDetails(ticker), 182 + getTickerSnapshot(ticker), 183 + ]); 184 + 185 + return { detail: detail ?? undefined, snapshot }; 186 + } 187 + 188 + function formatDate(date: Date): string { 189 + return date.toISOString().split('T')[0]; 190 + } 191 + 192 + export async function getAggregateSeries( 193 + ticker: string, 194 + timeframe: StockTimeframe, 195 + ): Promise<StockAggregatePoint[]> { 196 + const config = TIMEFRAME_CONFIG[timeframe]; 197 + if (!config) { 198 + throw new Error(`Unsupported timeframe: ${timeframe}`); 199 + } 200 + 201 + const now = new Date(); 202 + const fetchAggregates = async (extraDays: number) => { 203 + const fromDate = new Date(now.getTime() - (config.daysBack + extraDays) * DAY_MS); 204 + return withClient((client) => 205 + client.getStocksAggregates( 206 + ticker.trim().toUpperCase(), 207 + config.multiplier, 208 + config.timespan, 209 + formatDate(fromDate), 210 + formatDate(now), 211 + true, 212 + GetStocksAggregatesSortEnum.Asc, 213 + config.limit, 214 + ), 215 + ); 216 + }; 217 + 218 + try { 219 + let response: GetStocksAggregates200Response = await fetchAggregates(0); 220 + 221 + if ((!response.results || response.results.length === 0) && timeframe === '1d') { 222 + response = await fetchAggregates(5); 223 + } 224 + 225 + const rawResults = response.results ?? []; 226 + if (!rawResults.length) { 227 + return []; 228 + } 229 + 230 + let filteredResults = rawResults.filter( 231 + (result) => typeof result.t === 'number' && typeof result.c === 'number', 232 + ); 233 + 234 + if (config.displayWindowMs && filteredResults.length) { 235 + const latestTimestamp = filteredResults[filteredResults.length - 1].t!; 236 + const cutoff = latestTimestamp - config.displayWindowMs; 237 + filteredResults = filteredResults.filter((result) => result.t! >= cutoff); 238 + } 239 + 240 + return filteredResults.map((result) => ({ 241 + timestamp: result.t!, 242 + open: result.o ?? result.c ?? 0, 243 + high: result.h ?? result.c ?? 0, 244 + low: result.l ?? result.c ?? 0, 245 + close: result.c ?? 0, 246 + volume: result.v ?? 0, 247 + vwap: result.vw, 248 + })); 249 + } catch (error) { 250 + if (isNotFoundError(error)) { 251 + throw new Error('STOCKS_TICKER_NOT_FOUND'); 252 + } 253 + throw error; 254 + } 255 + } 256 + 257 + export function buildBrandingUrl(url?: string): string | undefined { 258 + if (!url) return undefined; 259 + 260 + try { 261 + const parsed = new URL(url); 262 + if (!parsed.searchParams.has('apiKey') && config.MASSIVE_API_KEY) { 263 + parsed.searchParams.set('apiKey', config.MASSIVE_API_KEY); 264 + } 265 + return parsed.toString(); 266 + } catch (error) { 267 + logger.warn('Failed to parse branding URL', { url, error }); 268 + return undefined; 269 + } 270 + } 271 + 272 + export function sanitizeTickerInput(input: string): string { 273 + return input 274 + .trim() 275 + .toUpperCase() 276 + .replace(/[^A-Z0-9.-]/g, '') 277 + .slice(0, 12); 278 + }
-2
src/services/social/fetchers/index.ts
··· 1 - export { BlueskyFetcher, FediverseFetcher, UnifiedFetcher } from './UnifiedFetcher'; 2 - export type { SocialMediaFetcher } from '../../../types/social';
+2 -2
src/types/base.ts
··· 1 1 export interface RandomReddit { 2 2 response_time_ms: number; 3 - source: 'reddit'; 3 + source: string; 4 4 subreddit: string; 5 5 title: string; 6 - upvotes: 71; 6 + upvotes: number; 7 7 url: string; 8 8 } 9 9
+224
src/types/componentsV2.ts
··· 1 + export type V2ComponentType = 3 | 4 | 10 | 12 | 18; 2 + 3 + export interface V2BaseComponent { 4 + type: V2ComponentType; 5 + } 6 + 7 + export interface V2StringSelect extends V2BaseComponent { 8 + type: 3; 9 + custom_id: string; 10 + placeholder?: string; 11 + options: Array<{ 12 + label: string; 13 + value: string; 14 + description?: string; 15 + emoji?: { id?: string; name?: string; animated?: boolean }; 16 + }>; 17 + min_values?: number; 18 + max_values?: number; 19 + } 20 + 21 + export interface V2TextInput extends V2BaseComponent { 22 + type: 4; 23 + custom_id: string; 24 + style: 1 | 2; 25 + label?: string; 26 + placeholder?: string; 27 + required?: boolean; 28 + min_length?: number; 29 + max_length?: number; 30 + value?: string; 31 + } 32 + 33 + export interface V2LabeledComponent extends V2BaseComponent { 34 + type: 18; 35 + label?: string; 36 + description?: string; 37 + component: V2StringSelect | V2TextInput; 38 + } 39 + 40 + export type V2ModalRow = 41 + | V2LabeledComponent 42 + | { type: 1; components: V2TextInput[] } 43 + | { type: 18; components: V2TextInput[] }; 44 + 45 + export interface V2SubmissionValueMap { 46 + [customId: string]: string | string[]; 47 + } 48 + 49 + export interface V2ModalPayload { 50 + custom_id: string; 51 + title: string; 52 + components: V2ModalRow[]; 53 + } 54 + 55 + interface V2Component { 56 + type: number; 57 + custom_id?: string; 58 + customId?: string; 59 + value?: string | string[]; 60 + values?: string[]; 61 + [key: string]: unknown; 62 + } 63 + 64 + interface V2Row { 65 + component?: V2Component; 66 + components?: V2Component[]; 67 + type?: number; 68 + [key: string]: unknown; 69 + } 70 + 71 + export function buildProviderModal(customId: string, title: string): V2ModalPayload { 72 + return { 73 + custom_id: customId, 74 + title, 75 + components: [ 76 + { 77 + type: 18, 78 + label: 'AI Provider', 79 + description: 'Select an authorized provider', 80 + component: { 81 + type: 3, 82 + custom_id: 'provider', 83 + placeholder: 'Choose provider', 84 + options: [ 85 + { label: 'OpenAI', value: 'openai', description: 'api.openai.com' }, 86 + { label: 'Anthropic', value: 'anthropic', description: 'api.anthropic.com' }, 87 + { label: 'OpenRouter', value: 'openrouter', description: 'openrouter.ai' }, 88 + { 89 + label: 'Google Gemini', 90 + value: 'gemini', 91 + description: 'generativelanguage.googleapis.com', 92 + }, 93 + { label: 'DeepSeek', value: 'deepseek', description: 'api.deepseek.com' }, 94 + { label: 'Moonshot AI', value: 'moonshot', description: 'api.moonshot.ai' }, 95 + { label: 'Perplexity AI', value: 'perplexity', description: 'api.perplexity.ai' }, 96 + ], 97 + }, 98 + }, 99 + { 100 + type: 1 as const, 101 + components: [ 102 + { 103 + type: 4, 104 + custom_id: 'model', 105 + label: 'Model', 106 + style: 1, 107 + required: true, 108 + placeholder: 'openai/gpt-4o-mini', 109 + min_length: 2, 110 + max_length: 100, 111 + }, 112 + ], 113 + }, 114 + { 115 + type: 1 as const, 116 + components: [ 117 + { 118 + type: 4, 119 + custom_id: 'apiKey', 120 + label: 'API Key', 121 + style: 1, 122 + required: true, 123 + placeholder: 'sk-... or other', 124 + min_length: 10, 125 + max_length: 500, 126 + }, 127 + ], 128 + }, 129 + ], 130 + }; 131 + } 132 + 133 + interface RawModalSubmission { 134 + fields?: { 135 + getTextInputValue?: (id: string) => string; 136 + [key: string]: unknown; 137 + }; 138 + data?: { 139 + components?: Array<{ 140 + component?: V2Component; 141 + components?: V2Component[]; 142 + [key: string]: unknown; 143 + }>; 144 + [key: string]: unknown; 145 + }; 146 + components?: Array<{ 147 + component?: V2Component; 148 + components?: V2Component[]; 149 + [key: string]: unknown; 150 + }>; 151 + message?: { 152 + components?: Array<{ 153 + component?: V2Component; 154 + components?: V2Component[]; 155 + [key: string]: unknown; 156 + }>; 157 + [key: string]: unknown; 158 + }; 159 + [key: string]: unknown; 160 + } 161 + 162 + export function parseV2ModalSubmission(raw: RawModalSubmission): V2SubmissionValueMap { 163 + const result: V2SubmissionValueMap = {}; 164 + try { 165 + const fields = raw?.fields as { getTextInputValue?: (id: string) => string } | undefined; 166 + if (fields?.getTextInputValue) { 167 + for (const id of ['model', 'apiKey']) { 168 + try { 169 + const v = fields.getTextInputValue(id); 170 + if (v !== undefined && v !== '') result[id] = v; 171 + } catch (error) { 172 + console.error(`Error getting text input value for ${id}:`, error); 173 + } 174 + } 175 + } 176 + } catch (error) { 177 + console.error('Error processing text input fields:', error); 178 + } 179 + 180 + try { 181 + const mergedRows = ( 182 + [ 183 + ...(raw?.data?.components || []), 184 + ...(raw?.components || []), 185 + ...(raw?.message?.components || []), 186 + ] as Array<V2Row | undefined> 187 + ).filter(Boolean) as V2Row[]; 188 + 189 + const flat = mergedRows.flatMap((r) => (r.component ? [r.component] : r.components || [])); 190 + 191 + for (const c of flat) { 192 + if (!c) continue; 193 + 194 + if (c.customId === 'provider' || c.custom_id === 'provider') { 195 + if (Array.isArray((c as { values?: string[] }).values)) { 196 + result.provider = (c as { values: string[] }).values; 197 + } else if ((c as { value?: string | string[] }).value) { 198 + const value = (c as { value: string | string[] }).value; 199 + result.provider = Array.isArray(value) ? value : [value]; 200 + } 201 + } 202 + 203 + if (c.type === 4 && (c.custom_id || c.customId) && (c as { value?: unknown }).value) { 204 + const value = (c as { value: unknown }).value; 205 + if (typeof value === 'string') { 206 + result[c.custom_id || c.customId!] = value; 207 + } 208 + } 209 + } 210 + } catch (error) { 211 + console.error('Error processing component values:', error); 212 + } 213 + return result; 214 + } 215 + 216 + export const PROVIDER_TO_URL: Record<string, string> = { 217 + openai: 'https://api.openai.com/v1', 218 + anthropic: 'https://api.anthropic.com/v1', 219 + openrouter: 'https://openrouter.ai/api/v1', 220 + gemini: 'https://generativelanguage.googleapis.com', 221 + deepseek: 'https://api.deepseek.com', 222 + moonshot: 'https://api.moonshot.ai', 223 + perplexity: 'https://api.perplexity.ai', 224 + };
+692
src/utils/commandExecutor.ts
··· 1 + import { 2 + ChatInputCommandInteraction, 3 + InteractionReplyOptions, 4 + MessagePayload, 5 + EmbedBuilder, 6 + } from 'discord.js'; 7 + import { SlashCommandProps } from '@/types/command'; 8 + import BotClient from '@/services/Client'; 9 + import logger from '@/utils/logger'; 10 + 11 + export interface ToolCall { 12 + name: string; 13 + args: Record<string, unknown>; 14 + } 15 + 16 + type EmbedInput = Record<string, unknown> & { 17 + data?: Record<string, unknown>; 18 + toJSON?: () => unknown; 19 + title?: unknown; 20 + description?: unknown; 21 + fields?: unknown; 22 + color?: unknown; 23 + timestamp?: unknown; 24 + footer?: unknown; 25 + }; 26 + 27 + function toPlainEmbedObject(embed: unknown): Record<string, unknown> | unknown { 28 + if (embed && typeof embed === 'object') { 29 + const embedObj = embed as EmbedInput; 30 + if ('data' in embedObj && embedObj.data) { 31 + return embedObj.data as Record<string, unknown>; 32 + } 33 + if (typeof embedObj.toJSON === 'function') { 34 + return embedObj.toJSON(); 35 + } 36 + const e = embedObj as Record<string, unknown>; 37 + if ('title' in e || 'fields' in e || 'description' in e) { 38 + return e; 39 + } 40 + return { 41 + title: embedObj.title, 42 + description: embedObj.description, 43 + fields: embedObj.fields, 44 + color: embedObj.color, 45 + timestamp: embedObj.timestamp, 46 + footer: embedObj.footer, 47 + } as Record<string, unknown>; 48 + } 49 + return embed as Record<string, unknown>; 50 + } 51 + 52 + function testEmbedBuilderStructure() { 53 + const testEmbed = new EmbedBuilder() 54 + .setTitle('Test Title') 55 + .setDescription('Test Description') 56 + .addFields({ name: 'Test Field', value: 'Test Value', inline: true }); 57 + 58 + logger.debug(`[Command Executor] Test EmbedBuilder structure:`, { 59 + embed: testEmbed, 60 + hasData: 'data' in testEmbed, 61 + data: testEmbed.data, 62 + hasToJSON: typeof testEmbed.toJSON === 'function', 63 + toJSON: testEmbed.toJSON ? testEmbed.toJSON() : 'N/A', 64 + keys: Object.keys(testEmbed), 65 + prototype: Object.getPrototypeOf(testEmbed)?.constructor?.name, 66 + }); 67 + 68 + if (testEmbed.data) { 69 + logger.debug(`[Command Executor] Test embed data fields:`, { 70 + fields: testEmbed.data.fields, 71 + fieldsLength: testEmbed.data.fields?.length, 72 + allDataKeys: Object.keys(testEmbed.data), 73 + }); 74 + } 75 + } 76 + 77 + export function extractToolCalls(content: string): { cleanContent: string; toolCalls: ToolCall[] } { 78 + const toolCallRegex = /{([^{}\s:]+):({[^{}]*}|[^{}]*)?}/g; 79 + const toolCalls: ToolCall[] = []; 80 + let cleanContent = content; 81 + let match; 82 + 83 + while ((match = toolCallRegex.exec(content)) !== null) { 84 + try { 85 + if (!match[1]) { 86 + continue; 87 + } 88 + 89 + const toolName = match[1].trim(); 90 + const argsString = match[2] ? match[2].trim() : ''; 91 + 92 + if (!toolName) { 93 + continue; 94 + } 95 + 96 + let args: Record<string, unknown> = {}; 97 + 98 + if (argsString.startsWith('{') && argsString.endsWith('}')) { 99 + try { 100 + args = JSON.parse(argsString); 101 + } catch (_error) { 102 + args = { query: argsString }; 103 + } 104 + } else if (argsString) { 105 + if (argsString.startsWith('"') && argsString.endsWith('"')) { 106 + const unquoted = argsString.slice(1, -1); 107 + if (toolName === 'reaction') { 108 + args = { emoji: unquoted }; 109 + } else { 110 + args = { query: unquoted }; 111 + } 112 + } else { 113 + args = { query: argsString }; 114 + } 115 + } else { 116 + args = {}; 117 + } 118 + 119 + toolCalls.push({ 120 + name: toolName, 121 + args, 122 + }); 123 + 124 + cleanContent = cleanContent.replace(match[0], '').trim(); 125 + } catch (error) { 126 + logger.error(`Error parsing tool call: ${error}`); 127 + } 128 + } 129 + 130 + return { cleanContent, toolCalls }; 131 + } 132 + 133 + export async function executeToolCall( 134 + toolCall: ToolCall, 135 + interaction: ChatInputCommandInteraction, 136 + client: BotClient, 137 + ): Promise<string> { 138 + let { name, args } = toolCall; 139 + 140 + if (name.includes(':')) { 141 + const parts = name.split(':'); 142 + name = parts[0]; 143 + if (!args || Object.keys(args).length === 0) { 144 + args = { search: parts[1] }; 145 + } 146 + } 147 + 148 + try { 149 + const validCommands = ['cat', 'dog', 'joke', '8ball', 'weather', 'wiki']; 150 + 151 + if (!validCommands.includes(name.toLowerCase())) { 152 + throw new Error( 153 + `Command '${name}' is not a valid command. Available commands: ${validCommands.join(', ')}`, 154 + ); 155 + } 156 + 157 + if (['cat', 'dog'].includes(name)) { 158 + const commandName = name.charAt(0).toUpperCase() + name.slice(1); 159 + logger.debug(`[${commandName}] Starting ${name} command execution`); 160 + 161 + try { 162 + const commandDir = 'fun'; 163 + const commandModule = await import(`../commands/${commandDir}/${name}`); 164 + 165 + const imageData = 166 + name === 'cat' 167 + ? await commandModule.fetchCatImage() 168 + : await commandModule.fetchDogImage(); 169 + 170 + return JSON.stringify({ 171 + success: true, 172 + type: name, 173 + title: imageData.title || `Random ${commandName}`, 174 + url: imageData.url, 175 + subreddit: imageData.subreddit, 176 + source: name === 'cat' ? 'pur.cat' : 'erm.dog', 177 + handled: true, 178 + }); 179 + } catch (error) { 180 + const errorMessage = 181 + error instanceof Error ? error.message : `Unknown error in ${name} command`; 182 + logger.error(`[${commandName}] Error: ${errorMessage}`, { error }); 183 + return JSON.stringify({ 184 + success: false, 185 + error: `Failed to execute ${name} command: ${errorMessage}`, 186 + handled: false, 187 + }); 188 + } 189 + } 190 + 191 + if (name === 'wiki') { 192 + logger.debug('[Wiki] Starting wiki command execution'); 193 + 194 + try { 195 + const wikiModule = await import('../commands/utilities/wiki'); 196 + 197 + let searchQuery = ''; 198 + if (typeof args === 'object' && args !== null) { 199 + const argsObj = args as Record<string, unknown>; 200 + searchQuery = (argsObj.search as string) || (argsObj.query as string) || ''; 201 + } 202 + 203 + if (!searchQuery) { 204 + return JSON.stringify({ 205 + error: true, 206 + message: 'Missing search query', 207 + status: 400, 208 + }); 209 + } 210 + 211 + try { 212 + logger.debug( 213 + `[Wiki] Searching Wikipedia for: ${searchQuery} (locale: ${interaction.locale || 'en'})`, 214 + ); 215 + 216 + const searchResult = await wikiModule.searchWikipedia( 217 + searchQuery, 218 + interaction.locale || 'en', 219 + ); 220 + logger.debug(`[Wiki] Search result:`, { 221 + pageid: searchResult.pageid, 222 + title: searchResult.title, 223 + }); 224 + 225 + const article = await wikiModule.getArticleSummary( 226 + searchResult.pageid, 227 + searchResult.wikiLang, 228 + ); 229 + logger.debug(`[Wiki] Retrieved article:`, { 230 + title: article.title, 231 + extractLength: article.extract?.length, 232 + }); 233 + 234 + const maxLength = 1500; 235 + const truncated = article.extract && article.extract.length > maxLength; 236 + const extract = truncated 237 + ? article.extract.substring(0, maxLength) 238 + : article.extract || 'No summary available for this article.'; 239 + 240 + const response = { 241 + title: article.title, 242 + content: extract, 243 + url: `https://${searchResult.wikiLang}.wikipedia.org/wiki/${encodeURIComponent(article.title.replace(/ /g, '_'))}`, 244 + truncated: truncated, 245 + }; 246 + 247 + return JSON.stringify(response); 248 + } catch (error) { 249 + const errorMessage = error instanceof Error ? error.message : String(error); 250 + const stack = error instanceof Error ? error.stack : undefined; 251 + const responseStatus = 252 + error instanceof Error && 'response' in error 253 + ? (error as { response?: { status?: number } }).response?.status 254 + : undefined; 255 + 256 + logger.error('[Wiki] Error executing wiki command:', { 257 + error: errorMessage, 258 + stack, 259 + responseStatus, 260 + searchQuery, 261 + locale: interaction.locale, 262 + }); 263 + 264 + return JSON.stringify({ 265 + error: true, 266 + message: 267 + responseStatus === 404 268 + ? 'No Wikipedia article found for that search query. Please try a different search term.' 269 + : `Error searching Wikipedia: ${errorMessage}`, 270 + status: responseStatus || 500, 271 + }); 272 + } 273 + } catch (error) { 274 + const errorMessage = error instanceof Error ? error.message : String(error); 275 + const stack = error instanceof Error ? error.stack : undefined; 276 + logger.error('[Wiki] Error in wiki command execution:', { error: errorMessage, stack }); 277 + 278 + return JSON.stringify({ 279 + error: true, 280 + message: `Error in wiki command execution: ${errorMessage}`, 281 + status: 500, 282 + }); 283 + } 284 + } 285 + 286 + let commandModule; 287 + const isDev = process.env.NODE_ENV !== 'production'; 288 + const ext = isDev ? '.ts' : '.js'; 289 + 290 + try { 291 + let commandDir = 'fun'; 292 + if ( 293 + ['weather', 'wiki', 'ai', 'cobalt', 'remind', 'social', 'time', 'todo', 'whois'].includes( 294 + name, 295 + ) 296 + ) { 297 + commandDir = 'utilities'; 298 + } 299 + 300 + const commandPath = `../commands/${commandDir}/${name}${ext}`; 301 + logger.debug(`[Command Executor] Trying to import command from: ${commandPath}`); 302 + commandModule = await import(commandPath).catch((e) => { 303 + logger.error(`[Command Executor] Error importing command '${name}':`, e); 304 + throw e; 305 + }); 306 + 307 + if (name === 'cat' || name === 'dog') { 308 + try { 309 + const imageData = 310 + name === 'cat' 311 + ? await commandModule.fetchCatImage() 312 + : await commandModule.fetchDogImage(); 313 + 314 + return JSON.stringify({ 315 + success: true, 316 + type: name, 317 + title: imageData.title || `Random ${name === 'cat' ? 'Cat' : 'Dog'}`, 318 + url: imageData.url, 319 + subreddit: imageData.subreddit, 320 + source: name === 'cat' ? 'pur.cat' : 'erm.dog', 321 + }); 322 + } catch (error) { 323 + const errorMessage = 324 + error instanceof Error ? error.message : `Unknown error fetching ${name} image`; 325 + logger.error(`[${name}] Error: ${errorMessage}`, { error }); 326 + return JSON.stringify({ 327 + success: false, 328 + error: `Failed to fetch ${name} image: ${errorMessage}`, 329 + }); 330 + } 331 + } 332 + } catch (error) { 333 + logger.error(`[Command Executor] Error importing command '${name}':`, error); 334 + throw new Error(`Command '${name}' not found`); 335 + } 336 + 337 + const command = commandModule.default as SlashCommandProps; 338 + 339 + if (!command) { 340 + throw new Error(`Command '${name}' not found`); 341 + } 342 + 343 + let capturedResponse: unknown = null; 344 + 345 + const mockInteraction = { 346 + ...interaction, 347 + options: { 348 + getString: (param: string) => { 349 + if (typeof args === 'object' && args !== null) { 350 + const argsObj = args as Record<string, unknown>; 351 + return (argsObj[param] as string) || ''; 352 + } 353 + return ''; 354 + }, 355 + getNumber: (param: string) => { 356 + if (typeof args === 'object' && args !== null) { 357 + const argsObj = args as Record<string, unknown>; 358 + const value = argsObj[param]; 359 + return value !== null && value !== undefined ? Number(value) : null; 360 + } 361 + return null; 362 + }, 363 + getBoolean: (param: string) => { 364 + if (typeof args === 'object' && args !== null) { 365 + const argsObj = args as Record<string, unknown>; 366 + const value = argsObj[param]; 367 + return typeof value === 'boolean' ? value : null; 368 + } 369 + return null; 370 + }, 371 + }, 372 + deferReply: async () => { 373 + return Promise.resolve(); 374 + }, 375 + reply: async (options: InteractionReplyOptions | MessagePayload) => { 376 + if ('embeds' in options && options.embeds && options.embeds.length > 0) { 377 + const processedEmbeds = options.embeds.map((e) => toPlainEmbedObject(e)); 378 + capturedResponse = { ...(options as Record<string, unknown>), embeds: processedEmbeds }; 379 + } else { 380 + capturedResponse = options; 381 + } 382 + if ('embeds' in options && options.embeds && options.embeds.length > 0) { 383 + return JSON.stringify({ 384 + success: true, 385 + embeds: 386 + 'embeds' in (capturedResponse as Record<string, unknown>) 387 + ? (capturedResponse as { embeds?: unknown[] }).embeds 388 + : options.embeds, 389 + }); 390 + } 391 + return JSON.stringify({ 392 + success: true, 393 + content: 'content' in options ? options.content : undefined, 394 + }); 395 + }, 396 + editReply: async (options: InteractionReplyOptions | MessagePayload) => { 397 + if ('embeds' in options && options.embeds && options.embeds.length > 0) { 398 + const processedEmbeds = options.embeds.map((e) => toPlainEmbedObject(e)); 399 + capturedResponse = { ...(options as Record<string, unknown>), embeds: processedEmbeds }; 400 + } else { 401 + capturedResponse = options; 402 + } 403 + logger.debug(`[Command Executor] editReply called with options:`, { 404 + hasEmbeds: 'embeds' in options && options.embeds && options.embeds.length > 0, 405 + hasContent: 'content' in options, 406 + options: options, 407 + }); 408 + 409 + if ('embeds' in options && options.embeds && options.embeds.length > 0) { 410 + logger.debug(`[Command Executor] Raw embeds before processing:`, options.embeds); 411 + 412 + testEmbedBuilderStructure(); 413 + 414 + const processedEmbeds = options.embeds.map((embed) => { 415 + const embedObj = embed as EmbedInput; 416 + logger.debug(`[Command Executor] Processing embed:`, { 417 + hasData: !!(embedObj && typeof embedObj === 'object' && 'data' in embedObj), 418 + embedKeys: embedObj ? Object.keys(embedObj) : [], 419 + embedType: (embed as { constructor?: { name?: string } })?.constructor?.name, 420 + embedPrototype: Object.getPrototypeOf(embedObj as object)?.constructor?.name, 421 + }); 422 + const plain = toPlainEmbedObject(embedObj); 423 + return plain; 424 + }); 425 + 426 + logger.debug(`[Command Executor] Processed embeds:`, processedEmbeds); 427 + 428 + if (processedEmbeds.length > 0) { 429 + const firstEmbed = processedEmbeds[0]; 430 + const firstEmbedObj = firstEmbed as Record<string, unknown>; 431 + logger.debug(`[Command Executor] First processed embed details:`, { 432 + title: (firstEmbedObj as { title?: unknown })?.title, 433 + description: (firstEmbedObj as { description?: unknown })?.description, 434 + fields: (firstEmbedObj as { fields?: unknown })?.fields, 435 + fieldsLength: (firstEmbedObj as { fields?: Array<unknown> })?.fields?.length, 436 + allKeys: firstEmbedObj ? Object.keys(firstEmbedObj) : [], 437 + }); 438 + 439 + logger.debug( 440 + `[Command Executor] Full embed structure:`, 441 + JSON.stringify(firstEmbedObj, null, 2), 442 + ); 443 + } 444 + 445 + const response = { 446 + success: true, 447 + embeds: processedEmbeds, 448 + }; 449 + logger.debug(`[Command Executor] Returning embed response:`, response); 450 + return JSON.stringify(response); 451 + } 452 + const response = { 453 + success: true, 454 + content: 'content' in options ? options.content : undefined, 455 + }; 456 + logger.debug(`[Command Executor] Returning content response:`, response); 457 + return JSON.stringify(response); 458 + }, 459 + followUp: async (options: InteractionReplyOptions | MessagePayload) => { 460 + capturedResponse = options; 461 + return JSON.stringify({ 462 + success: true, 463 + content: 'content' in options ? options.content : undefined, 464 + }); 465 + }, 466 + } as unknown as ChatInputCommandInteraction; 467 + 468 + try { 469 + const result = await command.execute(client, mockInteraction); 470 + 471 + const responseToProcess = capturedResponse || result; 472 + 473 + logger.debug(`[Command Executor] Processing response for ${name}:`, { 474 + hasCapturedResponse: !!capturedResponse, 475 + hasResult: result !== undefined, 476 + responseType: typeof responseToProcess, 477 + }); 478 + 479 + if (!responseToProcess) { 480 + return JSON.stringify({ 481 + success: true, 482 + message: 'Command executed successfully', 483 + }); 484 + } 485 + 486 + if (typeof responseToProcess === 'string') { 487 + return JSON.stringify({ 488 + success: true, 489 + content: responseToProcess, 490 + }); 491 + } 492 + 493 + if (typeof responseToProcess === 'object') { 494 + const response = responseToProcess as Record<string, unknown>; 495 + 496 + if (Array.isArray(response.embeds) && response.embeds.length > 0) { 497 + const embeds = response.embeds as Array<{ 498 + title?: string; 499 + description?: string; 500 + fields?: Array<{ name: string; value: string; inline?: boolean }>; 501 + color?: number; 502 + timestamp?: string | number | Date; 503 + footer?: { text: string; icon_url?: string }; 504 + }>; 505 + 506 + logger.debug(`[Command Executor] Processing embeds for ${name}:`, { 507 + embedCount: embeds.length, 508 + firstEmbed: embeds[0], 509 + embedFields: embeds[0]?.fields, 510 + embedTitle: embeds[0]?.title, 511 + }); 512 + 513 + if (name === 'weather') { 514 + const embed = embeds[0]; 515 + logger.debug(`[Command Executor] Weather embed details:`, { 516 + hasEmbed: !!embed, 517 + hasFields: !!(embed && embed.fields), 518 + fieldsCount: embed?.fields?.length || 0, 519 + embedData: embed, 520 + }); 521 + 522 + if (embed && embed.fields && embed.fields.length > 0) { 523 + const f = embed.fields; 524 + const title = embed.title || ''; 525 + let locationFromTitle = ''; 526 + try { 527 + const m = title.match(/([A-Za-zร€-ร–ร˜-รถรธ-รฟ' .-]+,\s*[A-Z]{2})/u); 528 + if (m && m[1]) { 529 + locationFromTitle = m[1].trim(); 530 + } 531 + } catch (_err) { 532 + /* ignore */ 533 + } 534 + let locationArg = ''; 535 + if (args && typeof args === 'object') { 536 + const a = args as Record<string, unknown>; 537 + locationArg = String(a.location || a.query || a.search || '').trim(); 538 + } 539 + const weatherResponse = { 540 + success: true, 541 + type: 'weather', 542 + location: locationFromTitle || locationArg || 'Unknown location', 543 + temperature: f[0]?.value || 'N/A', 544 + feels_like: f[1]?.value || 'N/A', 545 + conditions: f[2]?.value || 'N/A', 546 + humidity: f[3]?.value || 'N/A', 547 + wind_speed: f[4]?.value || 'N/A', 548 + pressure: f[5]?.value || 'N/A', 549 + handled: true, 550 + }; 551 + logger.debug(`[Command Executor] Weather response:`, weatherResponse); 552 + return JSON.stringify(weatherResponse); 553 + } 554 + logger.debug(`[Command Executor] Weather embed has no fields, using fallback`); 555 + logger.debug(`[Command Executor] Weather embed fallback data:`, { 556 + title: embed?.title, 557 + description: embed?.description, 558 + fields: embed?.fields, 559 + allProperties: embed ? Object.keys(embed) : [], 560 + }); 561 + 562 + const fallbackResponse = { 563 + success: true, 564 + type: 'weather', 565 + location: 566 + embed?.title || 567 + (args && typeof args === 'object' 568 + ? String( 569 + (args as Record<string, unknown>).location || 570 + (args as Record<string, unknown>).query || 571 + (args as Record<string, unknown>).search || 572 + '', 573 + ) 574 + : '') || 575 + 'Unknown location', 576 + description: embed?.description || 'Weather data unavailable', 577 + rawEmbed: embed, 578 + handled: true, 579 + }; 580 + 581 + return JSON.stringify(fallbackResponse); 582 + } 583 + 584 + if (name === 'joke') { 585 + const embed = embeds[0]; 586 + return JSON.stringify({ 587 + success: true, 588 + type: 'joke', 589 + title: embed.title || 'Random Joke', 590 + setup: embed.description || 'No joke available', 591 + handled: true, 592 + }); 593 + } 594 + 595 + return JSON.stringify({ 596 + success: true, 597 + embeds: embeds.map((embed) => ({ 598 + title: embed.title, 599 + description: embed.description, 600 + fields: 601 + embed.fields?.map((f) => ({ 602 + name: f.name, 603 + value: f.value, 604 + inline: f.inline, 605 + })) || [], 606 + color: embed.color, 607 + timestamp: embed.timestamp, 608 + footer: embed.footer, 609 + })), 610 + }); 611 + } 612 + 613 + if (Array.isArray(response.components) && response.components.length > 0) { 614 + if (name === '8ball') { 615 + const components = response.components as Array<{ 616 + components?: Array<{ 617 + data?: { 618 + components?: Array<{ 619 + data?: { 620 + content?: string; 621 + }; 622 + }>; 623 + }; 624 + }>; 625 + }>; 626 + 627 + let question = ''; 628 + let answer = ''; 629 + 630 + for (const component of components) { 631 + if (component.components) { 632 + for (const subComponent of component.components) { 633 + if (subComponent.data?.components) { 634 + for (const textComponent of subComponent.data.components) { 635 + const content = textComponent.data?.content || ''; 636 + if (content.includes('**Question**') || content.includes('**Pregunta**')) { 637 + question = content 638 + .replace(/.*?\*\*(.*?)\*\*\s*>\s*(.*?)\n\n.*/s, '$2') 639 + .trim(); 640 + } else if ( 641 + content.includes('**Answer**') || 642 + content.includes('**Respuesta**') || 643 + content.includes('โœจ') 644 + ) { 645 + answer = content.replace(/.*?โœจ\s*(.*?)$/s, '$1').trim(); 646 + } 647 + } 648 + } 649 + } 650 + } 651 + } 652 + 653 + return JSON.stringify({ 654 + success: true, 655 + type: '8ball', 656 + question: question || 'Unknown question', 657 + answer: answer || 'Unknown answer', 658 + handled: true, 659 + }); 660 + } 661 + } 662 + 663 + if (typeof response.content === 'string') { 664 + return JSON.stringify({ 665 + success: true, 666 + content: response.content, 667 + }); 668 + } 669 + } 670 + 671 + return JSON.stringify({ 672 + success: true, 673 + message: 'Command executed successfully', 674 + }); 675 + } catch (error) { 676 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 677 + logger.error(`Error executing command '${name}':`, errorMessage); 678 + return JSON.stringify({ 679 + success: false, 680 + error: errorMessage, 681 + status: 500, 682 + }); 683 + } 684 + } catch (error) { 685 + logger.error(`Error executing tool call '${name}':`, error); 686 + return JSON.stringify({ 687 + success: false, 688 + error: `Error executing tool call '${name}': ${error}`, 689 + status: 500, 690 + }); 691 + } 692 + }
+1 -10
src/utils/encrypt.ts
··· 131 131 } 132 132 } 133 133 134 - function canDecrypt(encrypted: string): boolean { 135 - try { 136 - decrypt(encrypted); 137 - return true; 138 - } catch { 139 - return false; 140 - } 141 - } 142 - 143 134 function isValidEncryptedFormat(encrypted: string): boolean { 144 135 if (!encrypted || typeof encrypted !== 'string') { 145 136 return false; ··· 162 153 } 163 154 } 164 155 165 - export { encrypt, decrypt, canDecrypt, isValidEncryptedFormat, EncryptionError }; 156 + export { encrypt, decrypt, isValidEncryptedFormat, EncryptionError };
-82
src/utils/encryption.ts
··· 1 - import crypto from 'crypto'; 2 - import { API_KEY_ENCRYPTION_SECRET } from '../config'; 3 - 4 - const ENCRYPTION_KEY = API_KEY_ENCRYPTION_SECRET; 5 - const ALGORITHM = 'aes-256-gcm'; 6 - 7 - const getEncryptionKey = (): Buffer => { 8 - if (ENCRYPTION_KEY.length !== 32) { 9 - throw new Error('ENCRYPTION_KEY must be exactly 32 characters long'); 10 - } 11 - return Buffer.from(ENCRYPTION_KEY, 'utf8'); 12 - }; 13 - 14 - /** 15 - * Encrypts a string using AES-256-GCM 16 - * @param text The text to encrypt 17 - * @returns Base64 encoded encrypted data with IV and auth tag 18 - */ 19 - export const encryptApiKey = (text: string): string => { 20 - try { 21 - const key = getEncryptionKey(); 22 - const iv = crypto.randomBytes(16); 23 - 24 - const cipher = crypto.createCipheriv(ALGORITHM, key, iv); 25 - cipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); 26 - 27 - let encrypted = cipher.update(text, 'utf8', 'hex'); 28 - encrypted += cipher.final('hex'); 29 - 30 - const authTag = cipher.getAuthTag(); 31 - 32 - const combined = Buffer.concat([iv, authTag, Buffer.from(encrypted, 'hex')]); 33 - 34 - return combined.toString('base64'); 35 - } catch { 36 - throw new Error('Failed to encrypt API key'); 37 - } 38 - }; 39 - 40 - /** 41 - * Decrypts a string that was encrypted with encryptApiKey 42 - * @param encryptedData Base64 encoded encrypted data 43 - * @returns The decrypted text 44 - */ 45 - export const decryptApiKey = (encryptedData: string): string => { 46 - try { 47 - const key = getEncryptionKey(); 48 - const combined = Buffer.from(encryptedData, 'base64'); 49 - 50 - const extractedIv = combined.subarray(0, 16); 51 - const authTag = combined.subarray(16, 32); 52 - const encrypted = combined.subarray(32); 53 - 54 - const decipher = crypto.createDecipheriv(ALGORITHM, key, extractedIv); 55 - decipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); 56 - decipher.setAuthTag(authTag); 57 - 58 - let decrypted = decipher.update(encrypted, undefined, 'utf8'); 59 - decrypted += decipher.final('utf8'); 60 - 61 - return decrypted; 62 - } catch { 63 - throw new Error('Failed to decrypt API key'); 64 - } 65 - }; 66 - 67 - /** 68 - * Generates a secure random encryption key 69 - * @returns A 32-character random string suitable for use as ENCRYPTION_KEY 70 - */ 71 - export const generateEncryptionKey = (): string => { 72 - return crypto.randomBytes(32).toString('base64').substring(0, 32); 73 - }; 74 - 75 - /** 76 - * Validates that an encryption key is properly formatted 77 - * @param key The key to validate 78 - * @returns True if the key is valid 79 - */ 80 - export const validateEncryptionKey = (key: string): boolean => { 81 - return typeof key === 'string' && key.length === 32; 82 - };
+1 -1
src/utils/getGitCommitHash.ts
··· 40 40 } 41 41 42 42 initializeGitCommitHash().catch((error) => { 43 - console.warn('Failed to initialize git commit hash:', error.message); 43 + logger.warn('Failed to initialize git commit hash:', error.message); 44 44 }); 45 45 46 46 export default getGitCommitHash;
+11 -4
src/utils/logger.ts
··· 58 58 new winston.transports.Console({ 59 59 format: winston.format.combine( 60 60 winston.format.colorize(), 61 - winston.format.printf(({ timestamp, level, message, service, ...meta }) => { 62 - const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''; 63 - return `${timestamp} [${service}] ${level}: ${message} ${metaStr}`; 64 - }), 61 + winston.format.printf( 62 + ({ timestamp, level, message, service, environment, version, ...metadata }) => { 63 + let logMessage = `${timestamp} [${service}] [${environment}] [${version}] ${level}: ${message}`; 64 + 65 + if (Object.keys(metadata).length > 0) { 66 + logMessage += ` ${JSON.stringify(metadata)}`; 67 + } 68 + 69 + return logMessage; 70 + }, 71 + ), 65 72 ), 66 73 }), 67 74 ],
+395
src/utils/messageToolExecutor.ts
··· 1 + import { Message } from 'discord.js'; 2 + import BotClient from '@/services/Client'; 3 + import logger from '@/utils/logger'; 4 + import fetch from 'node-fetch'; 5 + 6 + interface WikipediaPage { 7 + pageid?: number; 8 + ns?: number; 9 + title: string; 10 + extract?: string; 11 + thumbnail?: { 12 + source: string; 13 + width: number; 14 + height: number; 15 + }; 16 + pageimage?: string; 17 + missing?: boolean; 18 + } 19 + 20 + interface _WikipediaResponse { 21 + query: { 22 + pages: Record<string, WikipediaPage>; 23 + }; 24 + } 25 + 26 + interface _WeatherResponse { 27 + main: { 28 + temp: number; 29 + feels_like: number; 30 + humidity: number; 31 + }; 32 + weather: Array<{ 33 + description: string; 34 + icon: string; 35 + }>; 36 + wind: { 37 + speed: number; 38 + }; 39 + name: string; 40 + sys: { 41 + country: string; 42 + }; 43 + } 44 + 45 + export interface MessageToolCall { 46 + name: string; 47 + args: Record<string, unknown>; 48 + } 49 + 50 + export interface ToolResult { 51 + content: Array<{ 52 + type: string; 53 + text?: string; 54 + image_url?: { 55 + url: string; 56 + detail?: 'low' | 'high' | 'auto'; 57 + }; 58 + }>; 59 + metadata: { 60 + type: string; 61 + url?: string; 62 + title?: string; 63 + subreddit?: string; 64 + source?: string; 65 + isSystem?: boolean; 66 + [key: string]: unknown; 67 + }; 68 + } 69 + 70 + function formatToolResponse( 71 + content: string, 72 + metadata: Record<string, unknown> = {}, 73 + showSystemMessage = true, 74 + ): ToolResult { 75 + if (showSystemMessage) { 76 + return { 77 + content: [ 78 + { 79 + type: 'text', 80 + text: `[SYSTEM] ${content}`, 81 + }, 82 + ], 83 + metadata: { 84 + ...metadata, 85 + type: 'tool_response', 86 + isSystem: true, 87 + }, 88 + }; 89 + } 90 + 91 + return { 92 + content: [], 93 + metadata: { 94 + ...metadata, 95 + type: 'tool_response', 96 + isSystem: false, 97 + }, 98 + }; 99 + } 100 + 101 + async function catTool(): Promise<ToolResult> { 102 + try { 103 + const res = await fetch('https://api.pur.cat/random-cat'); 104 + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); 105 + 106 + const data = (await res.json()) as { url: string; title?: string; subreddit?: string }; 107 + return formatToolResponse(`Here's a cute cat for you! ๐Ÿฑ\n\nImage URL: ${data.url}`, { 108 + type: 'cat', 109 + url: data.url, 110 + title: data.title, 111 + subreddit: data.subreddit, 112 + source: 'pur.cat', 113 + }); 114 + } catch (error) { 115 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 116 + logger.error('Error in cat tool:', error); 117 + throw new Error(`Failed to fetch cat image: ${errorMessage}`); 118 + } 119 + } 120 + 121 + async function dogTool(): Promise<ToolResult> { 122 + try { 123 + const headers = { 124 + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 125 + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 126 + }; 127 + 128 + const res = await fetch('https://api.erm.dog/random-dog', { headers }); 129 + if (!res.ok) throw new Error(`API request failed with status ${res.status}`); 130 + 131 + const data = (await res.json()) as { url: string; title?: string; subreddit?: string }; 132 + 133 + if (!data || !data.url) { 134 + throw new Error('Invalid response format from dog API'); 135 + } 136 + 137 + return formatToolResponse(`Here's a cute dog for you! ๐Ÿถ\n\nImage URL: ${data.url}`, { 138 + type: 'dog', 139 + url: data.url, 140 + title: data.title || 'Random Dog', 141 + subreddit: data.subreddit || 'dogpictures', 142 + source: 'erm.dog', 143 + }); 144 + } catch (error) { 145 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 146 + logger.error('Error in dog tool:', error); 147 + throw new Error(`Failed to fetch dog image: ${errorMessage}`); 148 + } 149 + } 150 + 151 + interface WikipediaSummary { 152 + title: string; 153 + extract: string; 154 + content_urls?: { 155 + desktop?: { 156 + page: string; 157 + }; 158 + }; 159 + } 160 + 161 + async function wikiTool(args: Record<string, unknown>): Promise<ToolResult> { 162 + const query = args.query as string; 163 + if (!query) { 164 + throw new Error('Query is required for wiki tool'); 165 + } 166 + 167 + try { 168 + const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`; 169 + const res = await fetch(url); 170 + 171 + if (!res.ok) { 172 + throw new Error(`Wikipedia API error: ${res.status} ${res.statusText}`); 173 + } 174 + 175 + const data = (await res.json()) as WikipediaSummary; 176 + const title = data.title || query; 177 + const extract = data.extract || 'No summary available.'; 178 + const pageUrl = 179 + data.content_urls?.desktop?.page || 180 + `https://en.wikipedia.org/wiki/${encodeURIComponent(query)}`; 181 + 182 + return formatToolResponse(`${title}\n\n${extract}\n\nSource: ${pageUrl}`, { 183 + type: 'wiki', 184 + title, 185 + extract, 186 + url: pageUrl, 187 + }); 188 + } catch (error) { 189 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 190 + logger.error('Error in wiki tool:', error); 191 + throw new Error(`Failed to search Wikipedia: ${errorMessage}`); 192 + } 193 + } 194 + 195 + interface WeatherData { 196 + main: { 197 + temp: number; 198 + feels_like: number; 199 + humidity: number; 200 + pressure: number; 201 + }; 202 + weather: Array<{ 203 + description: string; 204 + }>; 205 + wind: { 206 + speed: number; 207 + }; 208 + name: string; 209 + } 210 + 211 + async function weatherTool(args: Record<string, unknown>): Promise<ToolResult> { 212 + const location = args.location as string; 213 + if (!location) { 214 + throw new Error('Location is required for weather tool'); 215 + } 216 + 217 + const apiKey = process.env.OPENWEATHER_API_KEY; 218 + if (!apiKey) { 219 + throw new Error('OpenWeather API key not configured'); 220 + } 221 + 222 + try { 223 + const res = await fetch( 224 + `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&appid=${apiKey}&units=imperial`, 225 + ); 226 + 227 + if (!res.ok) { 228 + throw new Error(`Weather API error: ${res.status} ${res.statusText}`); 229 + } 230 + 231 + const data = (await res.json()) as WeatherData; 232 + const temp = Math.round(data.main.temp); 233 + const feels = Math.round(data.main.feels_like); 234 + const conditions = data.weather[0]?.description || 'Unknown'; 235 + const humidity = data.main.humidity; 236 + const wind = Math.round(data.wind.speed); 237 + const pressure = data.main.pressure; 238 + const city = data.name || location; 239 + 240 + return formatToolResponse( 241 + `Weather for ${city}: ${temp}ยฐF (feels ${feels}ยฐF), ${conditions}. ` + 242 + `Humidity ${humidity}%, Wind ${wind} mph, Pressure ${pressure} hPa.`, 243 + { 244 + type: 'weather', 245 + location: city, 246 + temperature: temp, 247 + feels_like: feels, 248 + conditions, 249 + humidity, 250 + wind_speed: wind, 251 + pressure, 252 + }, 253 + ); 254 + } catch (error) { 255 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 256 + logger.error('Error in weather tool:', error); 257 + throw new Error(`Failed to get weather: ${errorMessage}`); 258 + } 259 + } 260 + 261 + function resolveEmoji(input: string): string { 262 + const shortcode = input.match(/^:([a-z0-9_+-]+):$/i)?.[1]; 263 + if (shortcode) { 264 + const map: Record<string, string> = { 265 + thumbsup: '๐Ÿ‘', 266 + thumbsdown: '๐Ÿ‘Ž', 267 + '+1': '๐Ÿ‘', 268 + '-1': '๐Ÿ‘Ž', 269 + thumbs_up: '๐Ÿ‘', 270 + thumbs_down: '๐Ÿ‘Ž', 271 + heart: 'โค๏ธ', 272 + smile: '๐Ÿ˜„', 273 + grin: '๐Ÿ˜', 274 + joy: '๐Ÿ˜‚', 275 + cry: '๐Ÿ˜ข', 276 + sob: '๐Ÿ˜ญ', 277 + clap: '๐Ÿ‘', 278 + fire: '๐Ÿ”ฅ', 279 + star: 'โญ', 280 + eyes: '๐Ÿ‘€', 281 + tada: '๐ŸŽ‰', 282 + }; 283 + const key = shortcode.toLowerCase(); 284 + return map[key] || input; 285 + } 286 + return input; 287 + } 288 + 289 + async function reactionTool( 290 + args: Record<string, unknown>, 291 + message: Message, 292 + client: BotClient, 293 + opts?: { originalMessage?: Message; botMessage?: Message }, 294 + ): Promise<ToolResult> { 295 + const emoji = (args.emoji || args.query || '') as string; 296 + const target = ((args.target as string) || 'user').toLowerCase(); 297 + const targetMessage = target === 'bot' && opts?.botMessage ? opts.botMessage : message; 298 + 299 + if (!emoji) { 300 + throw new Error('Emoji is required for reaction tool'); 301 + } 302 + 303 + const resolvedEmoji = resolveEmoji(emoji); 304 + 305 + try { 306 + await targetMessage.react(resolvedEmoji); 307 + 308 + return { 309 + content: [], 310 + metadata: { 311 + type: 'reaction', 312 + emoji: resolvedEmoji, 313 + target, 314 + success: true, 315 + handled: true, 316 + isSystem: false, 317 + }, 318 + }; 319 + } catch (error) { 320 + logger.error('Failed to add reaction:', { emoji: resolvedEmoji, error }); 321 + throw new Error( 322 + `Failed to add reaction: ${error instanceof Error ? error.message : 'Unknown error'}`, 323 + ); 324 + } 325 + } 326 + 327 + type ToolFunction = ( 328 + args: Record<string, unknown>, 329 + message: Message, 330 + client: BotClient, 331 + opts?: { originalMessage?: Message; botMessage?: Message }, 332 + ) => Promise<ToolResult>; 333 + 334 + const TOOLS: Record<string, ToolFunction> = { 335 + cat: catTool, 336 + dog: dogTool, 337 + wiki: wikiTool, 338 + weather: weatherTool, 339 + reaction: (args, message, client, opts) => reactionTool(args, message, client, opts), 340 + newmessage: () => 341 + Promise.resolve({ 342 + content: [], 343 + metadata: { 344 + type: 'newmessage', 345 + success: true, 346 + handled: true, 347 + isSystem: false, 348 + }, 349 + }), 350 + }; 351 + 352 + export async function executeMessageToolCall( 353 + toolCall: MessageToolCall, 354 + message: Message, 355 + _client: BotClient, 356 + _opts?: { originalMessage?: Message; botMessage?: Message }, 357 + ): Promise<{ 358 + success: boolean; 359 + type: string; 360 + handled: boolean; 361 + error?: string; 362 + result?: ToolResult; 363 + }> { 364 + const name = (toolCall.name || '').toLowerCase(); 365 + 366 + try { 367 + const tool = TOOLS[name]; 368 + if (!tool) { 369 + return { 370 + success: false, 371 + type: 'error', 372 + handled: false, 373 + error: `Tool '${name}' not found`, 374 + }; 375 + } 376 + 377 + const result = await tool(toolCall.args || {}, message, _client, _opts); 378 + 379 + return { 380 + success: true, 381 + type: result.metadata.type || name, 382 + handled: true, 383 + result, 384 + }; 385 + } catch (error) { 386 + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 387 + logger.error(`Error in executeMessageToolCall for ${name}:`, error); 388 + return { 389 + success: false, 390 + type: 'error', 391 + handled: true, 392 + error: errorMessage, 393 + }; 394 + } 395 + }
-9
src/utils/misc.ts
··· 5 5 return array[Math.floor(Math.random() * array.length)]; 6 6 } 7 7 8 - export function iso2ToFlagEmoji(iso2: string): string { 9 - if (!iso2 || iso2.length !== 2) return ''; 10 - const upper = iso2.toUpperCase(); 11 - if (!/^[A-Z]{2}$/.test(upper)) return ''; 12 - const codePoints = upper.split('').map((char) => 0x1f1e6 + char.charCodeAt(0) - 65); 13 - if (codePoints.some((cp) => cp < 0x1f1e6 || cp > 0x1f1ff)) return ''; 14 - return String.fromCodePoint(...codePoints); 15 - } 16 - 17 8 export function iso2ToDiscordFlag(iso2: string): string { 18 9 if (!iso2 || iso2.length !== 2) return ''; 19 10 return `:flag_${iso2.toLowerCase()}:`;
+31
src/utils/rateLimiter.ts
··· 1 + type RateLimitedTask<T> = () => Promise<T> | T; 2 + 3 + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 4 + 5 + export function createRateLimiter(limitPerSecond: number) { 6 + const minDelay = Math.ceil(1000 / Math.max(1, limitPerSecond)); 7 + let lastRun = 0; 8 + let chain: Promise<void> = Promise.resolve(); 9 + 10 + const schedule = async <T>(task: RateLimitedTask<T>): Promise<T> => { 11 + const execute = chain.then(async () => { 12 + const elapsed = Date.now() - lastRun; 13 + const waitTime = lastRun === 0 ? 0 : Math.max(0, minDelay - elapsed); 14 + if (waitTime > 0) { 15 + await sleep(waitTime); 16 + } 17 + 18 + const result = await task(); 19 + lastRun = Date.now(); 20 + return result; 21 + }); 22 + 23 + chain = execute.then( 24 + () => undefined, 25 + () => undefined, 26 + ); 27 + return execute; 28 + }; 29 + 30 + return { schedule }; 31 + }
+86 -5
src/utils/sendDeploymentNotification.ts
··· 4 4 5 5 function getGitInfo() { 6 6 try { 7 - const commitHash = 8 - process.env.SOURCE_COMMIT || execSync('git rev-parse HEAD').toString().trim(); 9 - const commitMessage = execSync('git log -1 --pretty=%B').toString().trim(); 7 + try { 8 + execSync('git rev-parse --is-inside-work-tree'); 9 + 10 + try { 11 + execSync('git remote get-url origin'); 12 + } catch { 13 + execSync('git remote add origin https://github.com/aethel/aethel-labs'); 14 + } 15 + 16 + try { 17 + execSync('git fetch --depth=100 origin'); 18 + } catch (e) { 19 + logger.debug('git fetch failed or unnecessary; continuing', { 20 + error: (e as Error).message, 21 + }); 22 + } 23 + } catch (e) { 24 + logger.debug('Not a git repository; initializing temporary repo for metadata', { 25 + error: (e as Error).message, 26 + }); 27 + try { 28 + execSync('git init'); 29 + try { 30 + execSync('git remote add origin https://github.com/aethel/aethel-labs'); 31 + } catch (e) { 32 + logger.debug('origin remote already exists or cannot be added', { 33 + error: (e as Error).message, 34 + }); 35 + } 36 + const sourceCommit = process.env.SOURCE_COMMIT; 37 + if (sourceCommit) { 38 + try { 39 + execSync(`git fetch --depth=1 origin ${sourceCommit}`); 40 + } catch (err) { 41 + logger.debug('Failed to fetch SOURCE_COMMIT from origin', { 42 + error: (err as Error).message, 43 + }); 44 + } 45 + } else { 46 + try { 47 + const remoteHead = execSync('git ls-remote origin HEAD').toString().split('\t')[0]; 48 + if (remoteHead) { 49 + execSync(`git fetch --depth=1 origin ${remoteHead}`); 50 + process.env.SOURCE_COMMIT = remoteHead; 51 + } 52 + } catch (err) { 53 + logger.debug('Failed to resolve remote HEAD', { error: (err as Error).message }); 54 + } 55 + } 56 + } catch (err) { 57 + logger.debug('Failed to bootstrap temporary git repo', { error: (err as Error).message }); 58 + } 59 + } 60 + 61 + let commitHash: string | null = null; 62 + try { 63 + commitHash = process.env.SOURCE_COMMIT || execSync('git rev-parse HEAD').toString().trim(); 64 + } catch { 65 + try { 66 + const remoteHead = execSync('git ls-remote origin HEAD').toString().split('\t')[0]; 67 + commitHash = remoteHead || null; 68 + } catch (e) { 69 + logger.debug('Failed to resolve remote HEAD for commit hash', { 70 + error: (e as Error).message, 71 + }); 72 + } 73 + } 74 + 75 + const shortHash = commitHash ? commitHash.substring(0, 7) : 'unknown'; 76 + let commitMessage = 'No commit message'; 77 + try { 78 + commitMessage = commitHash 79 + ? execSync(`git log -1 --pretty=%B ${commitHash}`).toString().trim() 80 + : commitMessage; 81 + } catch (e) { 82 + logger.debug('Failed to resolve commit message', { error: (e as Error).message }); 83 + } 10 84 const branch = 11 85 process.env.GIT_BRANCH || 12 86 process.env.VERCEL_GIT_COMMIT_REF || 13 87 process.env.COOLIFY_BRANCH || 14 - execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); 88 + (() => { 89 + try { 90 + return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); 91 + } catch (e) { 92 + logger.debug('Failed to resolve branch', { error: (e as Error).message }); 93 + return 'unknown'; 94 + } 95 + })(); 15 96 16 97 return { 17 - commitHash: commitHash.substring(0, 7), 98 + commitHash: shortHash, 18 99 commitMessage, 19 100 branch, 20 101 };
+199
src/utils/stockChart.ts
··· 1 + import { createCanvas } from 'canvas'; 2 + import { StockAggregatePoint, StockTimeframe } from '@/services/massive'; 3 + 4 + const WIDTH = 900; 5 + const HEIGHT = 460; 6 + const PADDING = { 7 + top: 24, 8 + right: 32, 9 + bottom: 48, 10 + left: 64, 11 + }; 12 + const MIN_SPACING = 6; 13 + const UP_COLOR = '#1AC486'; 14 + const DOWN_COLOR = '#FF6B6B'; 15 + const GRID_COLOR = 'rgba(255,255,255,0.08)'; 16 + const AXIS_COLOR = 'rgba(255,255,255,0.4)'; 17 + const TEXT_COLOR = 'rgba(255,255,255,0.85)'; 18 + const BACKGROUND = '#0f1117'; 19 + 20 + const TIME_LABEL_FORMATTER = new Intl.DateTimeFormat('en-US', { 21 + hour: 'numeric', 22 + minute: '2-digit', 23 + }); 24 + const DATE_LABEL_FORMATTER = new Intl.DateTimeFormat('en-US', { 25 + month: 'short', 26 + day: 'numeric', 27 + }); 28 + const WEEKDAY_LABEL_FORMATTER = new Intl.DateTimeFormat('en-US', { 29 + weekday: 'short', 30 + month: 'short', 31 + day: 'numeric', 32 + }); 33 + 34 + function formatLabel(timestamp: number, timeframe?: StockTimeframe) { 35 + const date = new Date(timestamp); 36 + if (Number.isNaN(date.getTime())) return ''; 37 + if (timeframe === '1d') return TIME_LABEL_FORMATTER.format(date); 38 + if (timeframe === '5d') return WEEKDAY_LABEL_FORMATTER.format(date); 39 + return DATE_LABEL_FORMATTER.format(date); 40 + } 41 + 42 + export async function renderStockCandles( 43 + points: StockAggregatePoint[], 44 + timeframe?: StockTimeframe, 45 + ): Promise<Buffer> { 46 + if (!points.length) { 47 + throw new Error('No aggregate data available for chart'); 48 + } 49 + 50 + const maxCandlesMap: Record<StockTimeframe, number> = { 51 + '1d': 80, 52 + '5d': 110, 53 + '1m': 140, 54 + '3m': 160, 55 + '1y': 160, 56 + }; 57 + const fallbackLimit = 140; 58 + const limit = maxCandlesMap[timeframe ?? '1m'] ?? fallbackLimit; 59 + 60 + const sorted = points.slice(-limit).sort((a, b) => a.timestamp - b.timestamp); 61 + 62 + const chartWidth = WIDTH - PADDING.left - PADDING.right; 63 + const maxVisible = Math.max(3, Math.floor(chartWidth / MIN_SPACING)); 64 + const candles = sorted.slice(-maxVisible).map((point) => ({ 65 + x: point.timestamp, 66 + open: point.open, 67 + high: point.high, 68 + low: point.low, 69 + close: point.close, 70 + })); 71 + 72 + if (!candles.length) { 73 + throw new Error('No aggregate data available for chart'); 74 + } 75 + 76 + const canvas = createCanvas(WIDTH, HEIGHT); 77 + const ctx = canvas.getContext('2d'); 78 + ctx.antialias = 'subpixel'; 79 + 80 + ctx.fillStyle = BACKGROUND; 81 + ctx.fillRect(0, 0, WIDTH, HEIGHT); 82 + 83 + const values = candles.flatMap((candle) => [candle.high, candle.low]); 84 + const rawMax = Math.max(...values); 85 + const rawMin = Math.min(...values); 86 + 87 + const { niceMin, niceMax, tickSpacing } = computeNiceScale(rawMin, rawMax, 5); 88 + const maxPrice = niceMax; 89 + const minPrice = niceMin; 90 + const priceRange = maxPrice - minPrice || 1; 91 + 92 + const chartHeight = HEIGHT - PADDING.top - PADDING.bottom; 93 + 94 + const stepX = candles.length > 1 ? chartWidth / (candles.length - 1) : 0; 95 + const bodyWidth = 96 + candles.length > 1 ? Math.max(4, Math.min(18, stepX * 0.55)) : Math.min(24, chartWidth * 0.2); 97 + 98 + const mapY = (value: number) => 99 + PADDING.top + chartHeight - ((value - minPrice) / priceRange) * chartHeight; 100 + 101 + ctx.strokeStyle = GRID_COLOR; 102 + ctx.lineWidth = 1; 103 + ctx.font = '12px sans-serif'; 104 + ctx.fillStyle = TEXT_COLOR; 105 + 106 + const gridLines = Math.max(2, Math.round(priceRange / tickSpacing)); 107 + for (let i = 0; i <= gridLines; i++) { 108 + const value = maxPrice - tickSpacing * i; 109 + const clampedValue = Math.max(minPrice, Math.min(maxPrice, value)); 110 + const relative = (maxPrice - clampedValue) / priceRange; 111 + const y = PADDING.top + chartHeight * relative; 112 + ctx.beginPath(); 113 + ctx.moveTo(PADDING.left, y); 114 + ctx.lineTo(WIDTH - PADDING.right, y); 115 + ctx.stroke(); 116 + 117 + const priceLabel = clampedValue.toFixed(priceRange >= 10 ? 2 : 3); 118 + ctx.fillText(priceLabel, 16, y + 4); 119 + } 120 + 121 + ctx.strokeStyle = AXIS_COLOR; 122 + ctx.beginPath(); 123 + ctx.moveTo(PADDING.left, PADDING.top); 124 + ctx.lineTo(PADDING.left, HEIGHT - PADDING.bottom); 125 + ctx.lineTo(WIDTH - PADDING.right, HEIGHT - PADDING.bottom); 126 + ctx.stroke(); 127 + 128 + candles.forEach((candle, index) => { 129 + const x = candles.length === 1 ? PADDING.left + chartWidth / 2 : PADDING.left + stepX * index; 130 + const openY = mapY(candle.open); 131 + const closeY = mapY(candle.close); 132 + const highY = mapY(candle.high); 133 + const lowY = mapY(candle.low); 134 + const color = candle.close >= candle.open ? UP_COLOR : DOWN_COLOR; 135 + 136 + ctx.strokeStyle = color; 137 + ctx.lineWidth = 1.5; 138 + ctx.beginPath(); 139 + ctx.moveTo(x, highY); 140 + ctx.lineTo(x, lowY); 141 + ctx.stroke(); 142 + 143 + ctx.beginPath(); 144 + const bodyHeight = Math.max(2, Math.abs(closeY - openY)); 145 + const bodyTop = Math.min(openY, closeY); 146 + ctx.rect(x - bodyWidth / 2, bodyTop, bodyWidth, bodyHeight || 2); 147 + ctx.fillStyle = color; 148 + ctx.fill(); 149 + }); 150 + 151 + const labelCount = Math.min(6, candles.length); 152 + for (let i = 0; i < labelCount; i++) { 153 + const candleIndex = Math.round((i / Math.max(1, labelCount - 1)) * (candles.length - 1)); 154 + const label = formatLabel(candles[candleIndex].x, timeframe); 155 + const x = 156 + candles.length === 1 ? PADDING.left + chartWidth / 2 : PADDING.left + stepX * candleIndex; 157 + ctx.fillStyle = TEXT_COLOR; 158 + ctx.fillText(label, x - ctx.measureText(label).width / 2, HEIGHT - PADDING.bottom + 20); 159 + } 160 + 161 + return canvas.toBuffer('image/png'); 162 + } 163 + 164 + function computeNiceScale(min: number, max: number, maxTicks: number) { 165 + if (!Number.isFinite(min) || !Number.isFinite(max)) { 166 + return { niceMin: 0, niceMax: 1, tickSpacing: 0.2 }; 167 + } 168 + if (min === max) { 169 + const offset = Math.abs(min) * 0.05 || 1; 170 + min -= offset; 171 + max += offset; 172 + } 173 + 174 + const range = niceNum(max - min, false); 175 + const tickSpacing = niceNum(range / (maxTicks - 1), true); 176 + const niceMin = Math.floor(min / tickSpacing) * tickSpacing; 177 + const niceMax = Math.ceil(max / tickSpacing) * tickSpacing; 178 + 179 + return { niceMin, niceMax, tickSpacing }; 180 + } 181 + 182 + function niceNum(range: number, round: boolean) { 183 + const exponent = Math.floor(Math.log10(range)); 184 + const fraction = range / Math.pow(10, exponent); 185 + let niceFraction; 186 + 187 + if (round) { 188 + if (fraction < 1.5) niceFraction = 1; 189 + else if (fraction < 3) niceFraction = 2; 190 + else if (fraction < 7) niceFraction = 5; 191 + else niceFraction = 10; 192 + } else { 193 + if (fraction <= 1) niceFraction = 1; 194 + else if (fraction <= 2) niceFraction = 2; 195 + else if (fraction <= 5) niceFraction = 5; 196 + else niceFraction = 10; 197 + } 198 + return niceFraction * Math.pow(10, exponent); 199 + }
+14
src/utils/topgg.ts
··· 1 + const VOTE_COOLDOWN_HOURS = 12; 2 + 3 + export async function checkVoteStatus( 4 + _userId: string, 5 + ): Promise<{ hasVoted: boolean; nextVote: Date; voteCount: number }> { 6 + const now = new Date(); 7 + const nextVote = new Date(now.getTime() + VOTE_COOLDOWN_HOURS * 60 * 60 * 1000); 8 + 9 + return { 10 + hasVoted: true, 11 + nextVote, 12 + voteCount: 1, 13 + }; 14 + }
+1 -20
src/utils/userStrikes.ts
··· 16 16 } 17 17 } 18 18 19 - export async function getUserStrikeInfo(userId: string): Promise<StrikeInfo | null> { 19 + async function _getUserStrikeInfo(userId: string): Promise<StrikeInfo | null> { 20 20 if (!userId || typeof userId !== 'string') { 21 21 throw new StrikeError('Invalid user ID provided'); 22 22 } ··· 152 152 throw new StrikeError('Failed to reset old strikes'); 153 153 } 154 154 } 155 - 156 - export async function clearUserStrikes(userId: string): Promise<boolean> { 157 - if (!userId || typeof userId !== 'string') { 158 - throw new StrikeError('Invalid user ID provided'); 159 - } 160 - 161 - try { 162 - const res = await pgClient.query( 163 - 'UPDATE user_strikes SET strike_count = 0, banned_until = NULL WHERE user_id = $1', 164 - [userId], 165 - ); 166 - 167 - logger.info('Cleared user strikes', { userId }); 168 - return (res.rowCount ?? 0) > 0; 169 - } catch (error) { 170 - logger.error('Failed to clear user strikes', { userId, error }); 171 - throw new StrikeError('Failed to clear strikes', userId); 172 - } 173 - }
+29 -22
src/utils/validation.ts
··· 118 118 return validator.isFQDN(domain, { require_tld: true }); 119 119 } 120 120 121 - function normalizeInput(text: string): string { 122 - let normalized = text.toLowerCase(); 123 - normalized = normalized.replace(/([a-z])\1{2,}/g, '$1'); 124 - normalized = normalized 125 - .replace(/[@4]/g, 'a') 126 - .replace(/[3]/g, 'e') 127 - .replace(/[1!]/g, 'i') 128 - .replace(/[0]/g, 'o') 121 + function _normalizeText(text: string): string { 122 + if (!text) return ''; 123 + 124 + return text 125 + .toLowerCase() 126 + .replace(/([a-z])\1{2,}/g, '$1') 127 + .replace(/[^\w\s]/g, '') 128 + .replace(/@/g, 'a') 129 + .replace(/4/g, 'a') 130 + .replace(/3/g, 'e') 131 + .replace(/1|!/g, 'i') 132 + .replace(/0/g, 'o') 129 133 .replace(/[5$]/g, 's') 130 - .replace(/[7]/g, 't'); 131 - return normalized; 134 + .replace(/7/g, 't') 135 + .trim(); 132 136 } 133 137 134 138 export function getUnallowedWordCategory(text: string): string | null { 135 - const normalized = normalizeInput(text); 136 - for (const [category, words] of Object.entries(UNALLOWED_WORDS)) { 137 - for (const word of words as string[]) { 138 - if (category === 'slurs') { 139 - if (normalized.includes(word)) { 140 - return category; 141 - } 142 - } else { 143 - const pattern = new RegExp(`(?:^|\\W)${word}[a-z]{0,2}(?:\\W|$)`, 'i'); 144 - if (pattern.test(normalized)) { 145 - return category; 146 - } 139 + if (!text || typeof text !== 'string') return null; 140 + 141 + const words = text 142 + .toLowerCase() 143 + .split(/[\s"'.,?!;:]+/) 144 + .map((word) => word.replace(/[^\w\s]/g, '')) 145 + .filter((word) => word.length > 0); 146 + 147 + for (const word of words) { 148 + if (word.length <= 2) continue; 149 + 150 + for (const [category, wordList] of Object.entries(UNALLOWED_WORDS)) { 151 + if ((wordList as string[]).some((badWord) => word.toLowerCase() === badWord.toLowerCase())) { 152 + return category; 147 153 } 148 154 } 149 155 } 156 + 150 157 return null; 151 158 } 152 159
+215
src/utils/voteManager.ts
··· 1 + import pool from './pgClient'; 2 + import { Client, GatewayIntentBits } from 'discord.js'; 3 + import { checkVoteStatus } from './topgg'; 4 + import logger from './logger'; 5 + 6 + const VOTE_CREDITS = 10; 7 + const VOTE_COOLDOWN_HOURS = 12; 8 + 9 + export interface VoteResult { 10 + success: boolean; 11 + creditsAwarded: number; 12 + nextVoteAvailable: Date; 13 + } 14 + 15 + export interface CreditsInfo { 16 + remaining: number; 17 + lastReset: Date; 18 + } 19 + 20 + async function _hasVotedToday( 21 + userId: string, 22 + serverId?: string, 23 + ): Promise<{ hasVoted: boolean; nextVote: Date }> { 24 + try { 25 + const localResult = await pool.query<{ vote_timestamp: Date }>( 26 + `SELECT vote_timestamp FROM votes 27 + WHERE user_id = $1 28 + AND (server_id = $2 OR ($2 IS NULL AND server_id IS NULL)) 29 + ORDER BY vote_timestamp DESC 30 + LIMIT 1`, 31 + [userId, serverId], 32 + ); 33 + 34 + if (localResult.rows.length > 0) { 35 + const lastVote = new Date(localResult.rows[0].vote_timestamp); 36 + const nextVote = new Date(lastVote.getTime() + VOTE_COOLDOWN_HOURS * 60 * 60 * 1000); 37 + 38 + if (Date.now() < nextVote.getTime()) { 39 + return { hasVoted: true, nextVote }; 40 + } 41 + } 42 + 43 + if (process.env.TOPGG_TOKEN) { 44 + const voteStatus = await checkVoteStatus(userId); 45 + if (voteStatus.hasVoted) { 46 + await recordVoteInDatabase(userId, serverId); 47 + return { hasVoted: true, nextVote: voteStatus.nextVote }; 48 + } 49 + return { hasVoted: false, nextVote: voteStatus.nextVote }; 50 + } 51 + 52 + return { hasVoted: false, nextVote: new Date() }; 53 + } catch (error) { 54 + console.error('Error checking vote status:', error); 55 + const result = await pool.query( 56 + `SELECT 1 FROM votes 57 + WHERE user_id = $1 58 + AND (server_id = $2 OR ($2 IS NULL AND server_id IS NULL)) 59 + AND vote_timestamp > NOW() - INTERVAL '${VOTE_COOLDOWN_HOURS} hours'`, 60 + [userId, serverId], 61 + ); 62 + return { 63 + hasVoted: result.rows.length > 0, 64 + nextVote: new Date(Date.now() + VOTE_COOLDOWN_HOURS * 60 * 60 * 1000), 65 + }; 66 + } 67 + } 68 + 69 + async function recordVoteInDatabase(userId: string, serverId?: string): Promise<void> { 70 + const client = await pool.connect(); 71 + try { 72 + await client.query('BEGIN'); 73 + 74 + await client.query( 75 + `INSERT INTO votes (user_id, server_id, credits_awarded) 76 + VALUES ($1, $2, $3)`, 77 + [userId, serverId || null, VOTE_CREDITS], 78 + ); 79 + 80 + await client.query('COMMIT'); 81 + } catch (error) { 82 + await client.query('ROLLBACK'); 83 + throw error; 84 + } finally { 85 + client.release(); 86 + } 87 + } 88 + 89 + export async function recordVote( 90 + userId: string, 91 + serverId?: string, 92 + ): Promise<{ success: boolean; creditsAwarded: number; nextVoteAvailable: Date }> { 93 + const client = await pool.connect(); 94 + try { 95 + await client.query('BEGIN'); 96 + 97 + const existingVote = await client.query( 98 + `SELECT vote_timestamp FROM votes 99 + WHERE user_id = $1 100 + AND (server_id = $2 OR ($2 IS NULL AND server_id IS NULL)) 101 + ORDER BY vote_timestamp DESC 102 + LIMIT 1`, 103 + [userId, serverId || null], 104 + ); 105 + 106 + if (existingVote.rows.length > 0) { 107 + const lastVoteTime = new Date(existingVote.rows[0].vote_timestamp).getTime(); 108 + const cooldownEnd = lastVoteTime + VOTE_COOLDOWN_HOURS * 60 * 60 * 1000; 109 + 110 + if (Date.now() < cooldownEnd) { 111 + return { 112 + success: false, 113 + creditsAwarded: 0, 114 + nextVoteAvailable: new Date(cooldownEnd), 115 + }; 116 + } 117 + } 118 + 119 + const voteStatus = await checkVoteStatus(userId); 120 + if (!voteStatus.hasVoted) { 121 + return { 122 + success: false, 123 + creditsAwarded: 0, 124 + nextVoteAvailable: voteStatus.nextVote, 125 + }; 126 + } 127 + 128 + await client.query( 129 + `INSERT INTO votes (user_id, server_id, credits_awarded) 130 + VALUES ($1, $2, $3)`, 131 + [userId, serverId || null, VOTE_CREDITS], 132 + ); 133 + 134 + const clientBot = new Client({ 135 + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers], 136 + }); 137 + 138 + try { 139 + await clientBot.login(process.env.TOKEN); 140 + const user = await clientBot.users.fetch(userId); 141 + 142 + if (user) { 143 + const guilds = await clientBot.guilds.fetch(); 144 + await Promise.all( 145 + guilds.map(async (guild) => { 146 + try { 147 + const fullGuild = await guild.fetch(); 148 + const member = await fullGuild.members.fetch(userId).catch(() => null); 149 + 150 + if (member) { 151 + logger.debug( 152 + `User ${userId} is member of server ${guild.id} - vote benefits apply`, 153 + ); 154 + } 155 + } catch (error) { 156 + logger.error(`Error processing guild ${guild.id}:`, error); 157 + } 158 + }), 159 + ); 160 + } 161 + } catch (error) { 162 + logger.error('Error in vote processing:', error); 163 + } finally { 164 + clientBot.destroy().catch((err) => logger.error('Error destroying bot client:', err)); 165 + } 166 + 167 + logger.info(`User ${userId} voted - AI system will give +10 daily limit`); 168 + 169 + try { 170 + const clientBot = new Client({ 171 + intents: [ 172 + GatewayIntentBits.Guilds, 173 + GatewayIntentBits.GuildMembers, 174 + GatewayIntentBits.MessageContent, 175 + ], 176 + }); 177 + 178 + await clientBot.login(process.env.TOKEN); 179 + const user = await clientBot.users.fetch(userId); 180 + 181 + if (user) { 182 + const nextVoteTime = Math.floor((Date.now() + 12 * 60 * 60 * 1000) / 1000); 183 + await user 184 + .send( 185 + `๐ŸŽ‰ **Thank you for voting for Aethel!**\n` + 186 + `\n` + 187 + `You've received **+10 AI daily limit** for today!\n` + 188 + `\n` + 189 + `You can vote again <t:${nextVoteTime}:R>\n` + 190 + `\n` + 191 + `Thank you for your support! โค๏ธ`, 192 + ) 193 + .catch((err) => logger.error('Failed to send vote DM:', err)); 194 + } 195 + 196 + clientBot.destroy().catch((err) => logger.error('Error destroying bot client:', err)); 197 + } catch (error) { 198 + logger.error('Failed to send vote thank you DM:', error); 199 + } 200 + 201 + await client.query('COMMIT'); 202 + 203 + return { 204 + success: true, 205 + creditsAwarded: 10, 206 + nextVoteAvailable: voteStatus.nextVote, 207 + }; 208 + } catch (error) { 209 + await client.query('ROLLBACK'); 210 + logger.error('Error recording vote:', error); 211 + throw new Error('Failed to record your vote. Please try again later.'); 212 + } finally { 213 + client.release(); 214 + } 215 + }
+28 -1
web/src/lib/api.ts
··· 2 2 import { toast } from 'sonner'; 3 3 4 4 const api = axios.create({ 5 - baseURL: `${import.meta.env.VITE_FRONTEND_URL}/api`, 5 + baseURL: `/api`, 6 6 timeout: 10000, 7 7 }); 8 8 ··· 61 61 deleteApiKey: () => api.delete('/user/api-keys'), 62 62 testApiKey: (data: { apiKey: string; model?: string; apiUrl?: string }) => 63 63 api.post('/user/api-keys/test', data), 64 + getModels: async ({ apiKey, apiUrl }: { apiKey: string; apiUrl?: string }) => { 65 + try { 66 + let baseUrl = apiUrl || 'https://api.openai.com/v1'; 67 + if (!baseUrl.endsWith('/v1')) { 68 + baseUrl = baseUrl.endsWith('/') ? `${baseUrl}v1` : `${baseUrl}/v1`; 69 + } 70 + 71 + const response = await axios.get(`${baseUrl}/models`, { 72 + headers: { 73 + Authorization: `Bearer ${apiKey}`, 74 + 'Content-Type': 'application/json', 75 + }, 76 + timeout: 10000, 77 + }); 78 + 79 + interface ModelResponse { 80 + id: string; 81 + [key: string]: unknown; 82 + } 83 + 84 + const models = (response.data?.data as ModelResponse[])?.map((model) => model.id) || []; 85 + return { data: { models } }; 86 + } catch (error) { 87 + console.error('Failed to fetch models from provider API:', error); 88 + return { data: { models: [] } }; 89 + } 90 + }, 64 91 }; 65 92 66 93 export const remindersAPI = {
+387 -168
web/src/pages/ApiKeysPage.tsx
··· 1 - import { useState } from 'react'; 1 + import React, { useState, useEffect } from 'react'; 2 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 3 - import { Key, Eye, EyeOff, TestTube, Save, Trash2, AlertCircle, CheckCircle } from 'lucide-react'; 3 + import { 4 + Key, 5 + Eye, 6 + EyeOff, 7 + TestTube, 8 + Save, 9 + Trash2, 10 + AlertCircle, 11 + CheckCircle, 12 + Loader2, 13 + } from 'lucide-react'; 4 14 import { toast } from 'sonner'; 5 15 import { apiKeysAPI } from '../lib/api'; 6 16 17 + interface ApiKeyInfo { 18 + apiKey?: string; 19 + model?: string; 20 + apiUrl?: string; 21 + hasApiKey: boolean; 22 + } 23 + 24 + interface FormData { 25 + apiKey: string; 26 + model: string; 27 + apiUrl: string; 28 + } 29 + 30 + interface TestResult { 31 + success: boolean; 32 + message: string; 33 + } 34 + 7 35 const ApiKeysPage = () => { 8 36 const [showApiKey, setShowApiKey] = useState(false); 9 - const [formData, setFormData] = useState({ 37 + const [isLoadingModels, setIsLoadingModels] = useState(false); 38 + const [availableModels, setAvailableModels] = useState<string[]>([]); 39 + const [showModelDropdown, setShowModelDropdown] = useState(false); 40 + const [modelSearch, setModelSearch] = useState(''); 41 + const [isEditing, setIsEditing] = useState(false); 42 + const [hasPassedTest, setHasPassedTest] = useState(false); 43 + const [testResult, setTestResult] = useState<TestResult | null>(null); 44 + const [formData, setFormData] = useState<FormData>({ 10 45 apiKey: '', 11 46 model: '', 12 - apiUrl: '', 47 + apiUrl: 'https://api.openai.com/v1', 13 48 }); 14 - const [isEditing, setIsEditing] = useState(false); 15 - const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); 16 - const [hasPassedTest, setHasPassedTest] = useState(false); 17 - const queryClient = useQueryClient(); 18 49 19 - const { data: apiKeyInfo, isLoading } = useQuery({ 20 - queryKey: ['api-keys'], 21 - queryFn: () => apiKeysAPI.getApiKeys().then((res) => res.data), 22 - }); 50 + const filteredModels = React.useMemo(() => { 51 + if (!modelSearch) return availableModels; 52 + const searchTerm = modelSearch.toLowerCase(); 53 + return availableModels.filter((model) => model.toLowerCase().includes(searchTerm)); 54 + }, [availableModels, modelSearch]); 23 55 24 - const updateApiKeyMutation = useMutation({ 25 - mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => 26 - apiKeysAPI.updateApiKey(data), 27 - onSuccess: () => { 28 - queryClient.invalidateQueries({ queryKey: ['api-keys'] }); 29 - setIsEditing(false); 30 - setFormData({ apiKey: '', model: '', apiUrl: '' }); 31 - toast.success('API key updated successfully!'); 32 - }, 33 - onError: () => { 34 - toast.error('Failed to update API key'); 35 - }, 36 - }); 56 + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { 57 + const { name, value } = e.target; 58 + setFormData((prev) => ({ 59 + ...prev, 60 + [name]: value, 61 + })); 62 + }; 37 63 38 - const deleteApiKeyMutation = useMutation({ 39 - mutationFn: () => apiKeysAPI.deleteApiKey(), 40 - onSuccess: () => { 41 - queryClient.invalidateQueries({ queryKey: ['api-keys'] }); 42 - setFormData({ apiKey: '', model: '', apiUrl: '' }); 43 - setIsEditing(false); 44 - toast.success('API key deleted successfully!'); 45 - }, 46 - onError: () => { 47 - toast.error('Failed to delete API key'); 48 - }, 49 - }); 64 + const handleModelSelect = (model: string) => { 65 + setFormData((prev) => ({ 66 + ...prev, 67 + model, 68 + })); 69 + setModelSearch(''); 70 + setShowModelDropdown(false); 71 + }; 50 72 51 - const testApiKeyMutation = useMutation({ 52 - mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => 53 - apiKeysAPI.testApiKey(data), 54 - onSuccess: () => { 55 - setTestResult({ success: true, message: 'API key is valid and working!' }); 56 - setHasPassedTest(true); 57 - toast.success('API key test successful!'); 58 - }, 59 - onError: (error: { response?: { data?: { error?: string } } }) => { 60 - const message = error.response?.data?.error || 'API key test failed'; 61 - setTestResult({ success: false, message }); 62 - setHasPassedTest(false); 63 - toast.error(message); 64 - }, 65 - }); 73 + const handleModelInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { 74 + const value = e.target.value; 75 + setFormData((prev) => ({ ...prev, model: value })); 76 + setModelSearch(value); 77 + setHasPassedTest(false); 78 + setTestResult(null); 79 + setShowModelDropdown(true); 66 80 67 - const handleSubmit = (e: React.FormEvent) => { 68 - e.preventDefault(); 69 - if (!formData.apiKey.trim()) { 70 - toast.error('API key is required'); 71 - return; 81 + if (availableModels.length === 0 && formData.apiKey) { 82 + fetchModels(); 72 83 } 73 - if (!hasPassedTest) { 74 - toast.error('Please test the API key before saving'); 75 - return; 84 + }; 85 + 86 + const fetchModels = async () => { 87 + if (!formData.apiKey) return; 88 + 89 + setIsLoadingModels(true); 90 + try { 91 + const response = await apiKeysAPI.getModels({ 92 + apiKey: formData.apiKey, 93 + apiUrl: formData.apiUrl, 94 + }); 95 + 96 + if (response.data?.models?.length > 0) { 97 + setAvailableModels(response.data.models); 98 + } else { 99 + setAvailableModels([]); 100 + } 101 + } catch (error) { 102 + console.error('Error fetching models:', error); 103 + setAvailableModels([]); 104 + } finally { 105 + setIsLoadingModels(false); 76 106 } 77 - updateApiKeyMutation.mutate({ 78 - apiKey: formData.apiKey, 79 - model: formData.model || undefined, 80 - apiUrl: formData.apiUrl || undefined, 81 - }); 82 107 }; 83 108 84 - const handleTest = () => { 85 - if (!formData.apiKey.trim()) { 86 - toast.error('API key is required for testing'); 109 + const handleSubmit = async (e: React.FormEvent) => { 110 + e.preventDefault(); 111 + 112 + if (hasPassedTest) { 113 + updateApiKeyMutation.mutate({ 114 + apiKey: formData.apiKey, 115 + model: formData.model, 116 + apiUrl: formData.apiUrl, 117 + }); 87 118 return; 88 119 } 89 - setTestResult(null); 90 - testApiKeyMutation.mutate({ 91 - apiKey: formData.apiKey, 92 - model: formData.model || undefined, 93 - apiUrl: formData.apiUrl || undefined, 94 - }); 120 + 121 + try { 122 + const result = await testApiKeyMutation.mutateAsync({ 123 + apiKey: formData.apiKey, 124 + model: formData.model, 125 + apiUrl: formData.apiUrl, 126 + }); 127 + 128 + if (result.data?.success) { 129 + updateApiKeyMutation.mutate({ 130 + apiKey: formData.apiKey, 131 + model: formData.model, 132 + apiUrl: formData.apiUrl, 133 + }); 134 + } 135 + } catch (_error) { 136 + // ignore 137 + } 95 138 }; 96 139 97 140 const handleEdit = () => { 98 141 setIsEditing(true); 99 142 setFormData({ 100 - apiKey: '', 143 + apiKey: apiKeyInfo?.apiKey || '', 101 144 model: apiKeyInfo?.model || '', 102 - apiUrl: apiKeyInfo?.apiUrl || '', 145 + apiUrl: apiKeyInfo?.apiUrl || 'https://api.openai.com/v1', 103 146 }); 104 147 setTestResult(null); 105 148 setHasPassedTest(false); ··· 107 150 108 151 const handleCancel = () => { 109 152 setIsEditing(false); 110 - setFormData({ apiKey: '', model: '', apiUrl: '' }); 153 + setFormData({ 154 + apiKey: apiKeyInfo?.apiKey || '', 155 + model: apiKeyInfo?.model || '', 156 + apiUrl: apiKeyInfo?.apiUrl || 'https://api.openai.com/v1', 157 + }); 111 158 setTestResult(null); 112 159 setHasPassedTest(false); 113 160 }; 114 161 162 + const queryClient = useQueryClient(); 163 + 164 + const { data: apiKeyInfo, isLoading } = useQuery<ApiKeyInfo>({ 165 + queryKey: ['api-keys'], 166 + queryFn: () => apiKeysAPI.getApiKeys().then((res) => res.data as ApiKeyInfo), 167 + }); 168 + 169 + useEffect(() => { 170 + if (apiKeyInfo) { 171 + setFormData({ 172 + apiKey: apiKeyInfo.apiKey || '', 173 + model: apiKeyInfo.model || '', 174 + apiUrl: apiKeyInfo.apiUrl || 'https://api.openai.com/v1', 175 + }); 176 + } 177 + }, [apiKeyInfo]); 178 + 179 + const updateApiKeyMutation = useMutation({ 180 + mutationFn: (data: { apiKey?: string; model?: string; apiUrl?: string }) => 181 + apiKeysAPI.updateApiKey(data), 182 + onSuccess: () => { 183 + queryClient.invalidateQueries({ queryKey: ['api-keys'] }); 184 + toast.success('API key updated successfully'); 185 + setIsEditing(false); 186 + }, 187 + onError: (error: unknown) => { 188 + const errorMessage = 189 + error && typeof error === 'object' && 'response' in error 190 + ? (error as { response?: { data?: { error?: string } } })?.response?.data?.error 191 + : 'Failed to update API key'; 192 + toast.error(errorMessage); 193 + }, 194 + }); 195 + const testApiKeyMutation = useMutation({ 196 + mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => 197 + apiKeysAPI.testApiKey(data), 198 + onSuccess: async (_, variables) => { 199 + setHasPassedTest(true); 200 + setTestResult({ 201 + success: true, 202 + message: 'API key test successful!', 203 + }); 204 + toast.success('API key test successful!'); 205 + 206 + if (variables.apiKey) { 207 + try { 208 + setIsLoadingModels(true); 209 + const modelsResponse = await apiKeysAPI.getModels({ 210 + apiKey: variables.apiKey, 211 + apiUrl: variables.apiUrl, 212 + }); 213 + if (modelsResponse.data?.models?.length > 0) { 214 + setAvailableModels(modelsResponse.data.models); 215 + } else { 216 + setAvailableModels([]); 217 + } 218 + } catch (error) { 219 + console.error('Error fetching models:', error); 220 + setAvailableModels([]); 221 + } finally { 222 + setIsLoadingModels(false); 223 + } 224 + } 225 + }, 226 + onError: (error: unknown) => { 227 + const errorMessage = 228 + error && typeof error === 'object' && 'response' in error 229 + ? (error as { response?: { data?: { error?: string | { message: string } } } })?.response 230 + ?.data?.error 231 + : undefined; 232 + const message = 233 + typeof errorMessage === 'object' 234 + ? errorMessage?.message 235 + : errorMessage || 'API key test failed'; 236 + setHasPassedTest(false); 237 + setTestResult({ success: false, message }); 238 + toast.error(message); 239 + }, 240 + }); 241 + 242 + const deleteApiKeyMutation = useMutation({ 243 + mutationFn: () => apiKeysAPI.deleteApiKey(), 244 + onSuccess: () => { 245 + queryClient.invalidateQueries({ queryKey: ['api-keys'] }); 246 + toast.success('API key deleted successfully'); 247 + setIsEditing(false); 248 + setHasPassedTest(false); 249 + setTestResult(null); 250 + setFormData({ 251 + apiKey: '', 252 + model: '', 253 + apiUrl: 'https://api.openai.com/v1', 254 + }); 255 + }, 256 + onError: (error: unknown) => { 257 + const errorMessage = 258 + error && typeof error === 'object' && 'response' in error 259 + ? (error as { response?: { data?: { error?: string } } })?.response?.data?.error 260 + : 'Failed to delete API key'; 261 + toast.error(errorMessage); 262 + }, 263 + }); 264 + 115 265 if (isLoading) { 116 266 return ( 117 267 <div className="flex items-center justify-center h-64"> ··· 216 366 <span className="text-red-600 dark:text-red-400">*</span> You must test the API key 217 367 before saving to ensure it works correctly. 218 368 </p> 219 - <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 220 - API Key * 221 - </label> 222 - <div className="relative"> 223 - <input 224 - type={showApiKey ? 'text' : 'password'} 225 - value={formData.apiKey} 369 + 370 + <div className="mb-4"> 371 + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 372 + AI Provider * 373 + </label> 374 + <select 375 + value={formData.apiUrl} 226 376 onChange={(e) => { 227 - setFormData({ ...formData, apiKey: e.target.value }); 377 + const url = e.target.value; 378 + setFormData({ ...formData, apiUrl: url, model: '' }); 379 + setAvailableModels([]); 228 380 setHasPassedTest(false); 229 381 setTestResult(null); 230 382 }} 231 - placeholder="Enter your API key" 232 - className="input pr-10 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400" 233 - required 234 - /> 235 - <button 236 - type="button" 237 - onClick={() => setShowApiKey(!showApiKey)} 238 - className="absolute inset-y-0 right-0 pr-3 flex items-center" 383 + className="input dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 w-full" 239 384 > 240 - {showApiKey ? ( 241 - <EyeOff className="h-4 w-4 text-gray-400" /> 242 - ) : ( 243 - <Eye className="h-4 w-4 text-gray-400" /> 244 - )} 245 - </button> 385 + <option value="https://api.openai.com/v1">OpenAI (api.openai.com/v1)</option> 386 + <option value="https://openrouter.ai/api/v1"> 387 + OpenRouter (openrouter.ai/api/v1) 388 + </option> 389 + <option value="https://api.anthropic.com/v1"> 390 + Anthropic Claude (api.anthropic.com/v1) 391 + </option> 392 + <option value="https://api.mistral.ai/v1">Mistral AI (api.mistral.ai/v1)</option> 393 + <option value="https://api.deepseek.com/v1"> 394 + DeepSeek (api.deepseek.com/v1) 395 + </option> 396 + <option value="https://api.together.xyz/v1"> 397 + Together AI (api.together.xyz/v1) 398 + </option> 399 + <option value="https://api.perplexity.ai/v1"> 400 + Perplexity AI (api.perplexity.ai/v1) 401 + </option> 402 + <option value="https://generativelanguage.googleapis.com/v1beta"> 403 + Google Gemini (generativelanguage.googleapis.com) 404 + </option> 405 + <option value="https://api.groq.com/openai/v1"> 406 + Groq (api.groq.com/openai/v1) 407 + </option> 408 + <option value="https://api.lepton.ai/v1">Lepton AI (api.lepton.ai/v1)</option> 409 + <option value="https://api.deepinfra.com/v1/openai"> 410 + DeepInfra (api.deepinfra.com/v1/openai) 411 + </option> 412 + <option value="https://api.x.ai/v1">xAI (api.x.ai/v1)</option> 413 + <option value="https://api.moonshot.ai/v1"> 414 + Moonshot AI (api.moonshot.ai/v1) 415 + </option> 416 + </select> 417 + </div> 418 + 419 + <div className="mb-4"> 420 + <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 421 + API Key * 422 + </label> 423 + <div className="relative"> 424 + <input 425 + type={showApiKey ? 'text' : 'password'} 426 + name="apiKey" 427 + value={formData.apiKey} 428 + onChange={(e) => { 429 + handleInputChange(e); 430 + setHasPassedTest(false); 431 + setTestResult(null); 432 + }} 433 + placeholder="Enter your API key" 434 + className="input pr-10 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400 w-full" 435 + required 436 + /> 437 + <button 438 + type="button" 439 + onClick={() => setShowApiKey(!showApiKey)} 440 + className="absolute inset-y-0 right-0 pr-3 flex items-center" 441 + > 442 + {showApiKey ? ( 443 + <EyeOff className="h-4 w-4 text-gray-400" /> 444 + ) : ( 445 + <Eye className="h-4 w-4 text-gray-400" /> 446 + )} 447 + </button> 448 + </div> 246 449 </div> 247 450 </div> 248 451 249 - <div> 452 + <div className="relative"> 250 453 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 251 454 Model (Optional) 252 455 </label> 253 - <input 254 - type="text" 255 - value={formData.model} 256 - onChange={(e) => { 257 - setFormData({ ...formData, model: e.target.value }); 258 - setHasPassedTest(false); 259 - setTestResult(null); 260 - }} 261 - placeholder="e.g., openai/gpt-4o-mini, anthropic/claude-4-sonnet" 262 - className="input dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400" 263 - /> 264 - <p className="text-xs text-gray-600 dark:text-gray-400 mt-1"> 265 - Leave empty to use the default model 266 - </p> 267 - </div> 268 - 269 - <div> 270 - <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 271 - API Endpoint URL (Optional) 272 - </label> 273 - <input 274 - type="url" 275 - value={formData.apiUrl} 276 - onChange={(e) => { 277 - setFormData({ ...formData, apiUrl: e.target.value }); 278 - setHasPassedTest(false); 279 - setTestResult(null); 280 - }} 281 - placeholder="https://openrouter.ai/api/v1" 282 - className="input dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400" 283 - /> 284 - <p className="text-xs text-gray-600 dark:text-gray-400 mt-1"> 285 - Enter the base API URL (e.g., https://openrouter.ai/api/v1, 286 - https://api.openai.com/v1) 287 - </p> 456 + <div className="relative"> 457 + <input 458 + type="text" 459 + name="model" 460 + value={formData.model} 461 + onFocus={() => setShowModelDropdown(true)} 462 + onChange={handleModelInputChange} 463 + onBlur={() => { 464 + setTimeout(() => setShowModelDropdown(false), 200); 465 + }} 466 + placeholder="Select or type a model name" 467 + className="model-input input dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400 w-full pr-8" 468 + /> 469 + {isLoadingModels && ( 470 + <div className="absolute right-3 top-1/2 -translate-y-1/2"> 471 + <Loader2 className="h-4 w-4 animate-spin text-gray-400" /> 472 + </div> 473 + )} 474 + {showModelDropdown && (availableModels.length > 0 || modelSearch) && ( 475 + <div className="absolute z-10 mt-1 w-full rounded-md bg-white dark:bg-gray-800 shadow-lg border border-gray-200 dark:border-gray-700 max-h-60 overflow-auto"> 476 + {filteredModels.length > 0 ? ( 477 + filteredModels.map((model: string) => ( 478 + <div 479 + key={model} 480 + className="px-4 py-2 text-sm text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" 481 + onMouseDown={(e) => { 482 + e.preventDefault(); 483 + handleModelSelect(model); 484 + }} 485 + > 486 + {model} 487 + </div> 488 + )) 489 + ) : ( 490 + <div className="px-4 py-2 text-sm text-gray-500 dark:text-gray-400"> 491 + No matching models found 492 + </div> 493 + )} 494 + </div> 495 + )} 496 + <p className="text-xs text-gray-600 dark:text-gray-400 mt-1"> 497 + {availableModels.length > 0 498 + ? `Found ${availableModels.length} models` 499 + : 'Type to search or leave empty for default'} 500 + </p> 501 + </div> 288 502 </div> 289 503 290 504 {testResult && ( ··· 304 518 </div> 305 519 )} 306 520 307 - <div className="flex flex-col sm:flex-row sm:justify-between gap-3 pt-4"> 521 + <div className="flex flex-col sm:flex-row justify-end gap-3 pt-4"> 308 522 <button 309 523 type="button" 310 - onClick={handleTest} 311 - disabled={!formData.apiKey.trim() || testApiKeyMutation.isPending} 312 - className="btn btn-secondary active:scale-95 transition-transform order-1 sm:order-none" 524 + onClick={handleCancel} 525 + className="btn btn-secondary active:scale-95 transition-transform" 526 + disabled={updateApiKeyMutation.isPending || testApiKeyMutation.isPending} 527 + > 528 + Cancel 529 + </button> 530 + <button 531 + type="submit" 532 + disabled={ 533 + !formData.apiKey.trim() || 534 + updateApiKeyMutation.isPending || 535 + testApiKeyMutation.isPending 536 + } 537 + className={`btn active:scale-95 transition-transform ${ 538 + hasPassedTest ? 'btn-primary' : 'btn-secondary' 539 + }`} 313 540 > 314 - <TestTube className="h-4 w-4 mr-2" /> 315 - {testApiKeyMutation.isPending ? 'Testing...' : 'Test API Key'} 541 + {testApiKeyMutation.isPending ? ( 542 + <> 543 + <Loader2 className="h-4 w-4 mr-2 animate-spin" /> 544 + Testing... 545 + </> 546 + ) : updateApiKeyMutation.isPending ? ( 547 + <> 548 + <Loader2 className="h-4 w-4 mr-2 animate-spin" /> 549 + Saving... 550 + </> 551 + ) : hasPassedTest ? ( 552 + <> 553 + <Save className="h-4 w-4 mr-2" /> 554 + Save 555 + </> 556 + ) : ( 557 + <> 558 + <TestTube className="h-4 w-4 mr-2" /> 559 + Test & Save 560 + </> 561 + )} 316 562 </button> 317 - 318 - <div className="flex flex-col sm:flex-row gap-3 sm:space-x-3 sm:gap-0"> 319 - <button 320 - type="button" 321 - onClick={handleCancel} 322 - className="btn btn-secondary active:scale-95 transition-transform" 323 - disabled={updateApiKeyMutation.isPending} 324 - > 325 - Cancel 326 - </button> 327 - <button 328 - type="submit" 329 - disabled={ 330 - !formData.apiKey.trim() || updateApiKeyMutation.isPending || !hasPassedTest 331 - } 332 - className={`btn active:scale-95 transition-transform ${ 333 - hasPassedTest ? 'btn-primary' : 'btn-secondary opacity-50 cursor-not-allowed' 334 - }`} 335 - > 336 - <Save className="h-4 w-4 mr-2" /> 337 - {updateApiKeyMutation.isPending 338 - ? 'Saving...' 339 - : hasPassedTest 340 - ? 'Save' 341 - : 'Test Required'} 342 - </button> 343 - </div> 344 563 </div> 345 564 </form> 346 565 </div>
+1 -1
web/src/pages/LandingPage.tsx
··· 205 205 <div className="mb-4"> 206 206 <p className="text-xs text-gray-600 dark:text-gray-400 mb-2">Powered by</p> 207 207 <a 208 - href="https://royalehosting.net?utm_source=aethel.xyz&utm_medium=referral&utm_campaign=powered_by&utm_content=footer" 208 + href="https://royalehosting.net/?aff=8033?utm_source=aethel.xyz&utm_medium=referral&utm_campaign=powered_by&utm_content=footer" 209 209 target="_blank" 210 210 rel="noopener noreferrer" 211 211 className="inline-block hover:opacity-80 transition-opacity"
+2 -2
web/src/pages/LoginPage.tsx
··· 28 28 29 29 const handleDiscordLogin = async () => { 30 30 try { 31 - window.location.href = `${import.meta.env.VITE_FRONTEND_URL}/api/auth/discord`; 31 + window.location.href = `/api/auth/discord`; 32 32 } catch (_error) { 33 33 toast.error('Failed to initiate Discord login'); 34 34 } ··· 66 66 67 67 <button 68 68 onClick={handleDiscordLogin} 69 - className="w-full flex items-center justify-center px-8 py-3 border border-transparent rounded-full shadow-sm text-white bg-[#5865F2] hover:bg-[#4752c4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#5865F2] transition-all transform hover:scale-105 font-bold shadow-lg hover:shadow-xl" 69 + className="w-full flex items-center justify-center px-8 py-3 border border-transparent rounded-full shadow-sm text-white bg-[#5865F2] hover:bg-[#4752c4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#5865F2] transition-all transform hover:scale-105 font-bold hover:shadow-xl" 70 70 > 71 71 <svg 72 72 className="w-5 h-5 mr-3"
+1 -1
web/src/pages/PrivacyPage.tsx
··· 178 178 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 179 179 We implement the following security measures to protect your information: 180 180 </p> 181 - <ul className="list-disc pl-6 space-y-3 text-gray-700 mt-2"> 181 + <ul className="list-disc pl-6 space-y-3 text-gray-700 dark:text-gray-300 mt-2"> 182 182 <li> 183 183 <strong>API Key Encryption:</strong> AES-256-GCM encryption for all stored API keys 184 184 </li>
+44 -7
web/src/pages/TermsPage.tsx
··· 4 4 return ( 5 5 <LegalLayout 6 6 title="Terms of Service" 7 - lastUpdated="June 16, 2025" 7 + lastUpdated="November 9, 2025" 8 8 > 9 9 <div className="space-y-8"> 10 10 <section> ··· 32 32 <li>Random cat and dog images</li> 33 33 <li>Weather information</li> 34 34 <li>Wiki lookups</li> 35 + <li>Informational stock snapshots</li> 35 36 <li>And other Discord utilities</li> 36 37 </ul> 37 38 </section> ··· 54 55 55 56 <section> 56 57 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 57 - 4. API Usage 58 + 4. Financial Data &amp; /stocks Command 59 + </h2> 60 + <div className="space-y-4 text-gray-700 dark:text-gray-300 leading-relaxed"> 61 + <p> 62 + The /stocks command and any other financial utilities are provided for informational 63 + purposes only. We do not offer investment advice, brokerage services, or any tools for 64 + trading automation. By using these features you acknowledge and agree that: 65 + </p> 66 + <ul className="list-disc pl-6 space-y-3"> 67 + <li> 68 + You will not rely on the Bot for investment, legal, tax, or other professional 69 + advice. 70 + </li> 71 + <li> 72 + You will not use the Bot to attempt to manipulate any financial market, coordinate 73 + trading activity, or distribute misleading information. 74 + </li> 75 + <li> 76 + All output is delayed, may be inaccurate, and is intended solely for personal, 77 + non-commercial use. 78 + </li> 79 + <li> 80 + You are solely responsible for complying with applicable securities laws, exchange 81 + policies, and platform rules. 82 + </li> 83 + <li> 84 + We may throttle, modify, or disable financial data access at any time without 85 + notice. 86 + </li> 87 + </ul> 88 + </div> 89 + </section> 90 + 91 + <section> 92 + <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 93 + 5. API Usage 58 94 </h2> 59 95 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 60 96 The Bot may use third-party APIs and services (&quot;Third-Party Services&quot;). Your ··· 63 99 <ul className="list-disc pl-6 space-y-3 text-gray-700 dark:text-gray-300 mt-2"> 64 100 <li>You are responsible for the security of your API keys</li> 65 101 <li> 66 - We do not store your API keys permanently - they are only kept in memory during your 67 - active session 102 + Your API keys are stored securely using industry-standard encryption and are only 103 + accessible to you 68 104 </li> 105 + <li>You can delete your API keys at any time through the API keys management page</li> 69 106 <li> 70 107 You must comply with the terms of service of any third-party APIs you use with the Bot 71 108 </li> ··· 78 115 79 116 <section> 80 117 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 81 - 5. Limitation of Liability 118 + 6. Limitation of Liability 82 119 </h2> 83 120 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 84 121 The Bot is provided &quot;as is&quot; without any warranties. We are not responsible for ··· 89 126 90 127 <section> 91 128 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 92 - 6. Changes to Terms 129 + 7. Changes to Terms 93 130 </h2> 94 131 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 95 132 We reserve the right to modify these terms at any time. Continued use of the Bot after ··· 99 136 100 137 <section> 101 138 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 102 - 7. Contact 139 + 8. Contact 103 140 </h2> 104 141 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 105 142 If you have any questions about these Terms of Service, please contact us at{' '}
+1 -1
web/src/stores/authStore.ts
··· 47 47 48 48 try { 49 49 axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 50 - const response = await axios.get(`${import.meta.env.VITE_FRONTEND_URL}/api/auth/me`); 50 + const response = await axios.get(`/api/auth/me`); 51 51 set({ 52 52 token, 53 53 user: response.data.user,
-2
web/src/vite-env.d.ts
··· 2 2 3 3 interface ImportMetaEnv { 4 4 readonly VITE_STATUS_API_KEY: string; 5 - readonly VITE_FRONTEND_URL: string; 6 - readonly VITE_DISCORD_CLIENT_ID: string; 7 5 } 8 6 9 7 interface ImportMeta {