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 ALLOWED_ORIGINS= 3 # Encryption secret for data stored in the Database (Like API keys) 4 API_KEY_ENCRYPTION_SECRET= 5 - # Client ID found in the Oauth page of the Discord Developer Portal 6 CLIENT_ID= 7 # Postgresql database URLs 8 DATABASE_URL= 9 # Openrouter API key for the default AI model 10 OPENROUTER_API_KEY= 11 # OpenWeather API key for the weather command 12 OPENWEATHER_API_KEY= 13 # Log level for the application (debug, info, warn, error) 14 LOG_LEVEL=info 15 # Node environment (development, production) ··· 18 SOURCE_COMMIT= 19 # The API key going to be used for the status API 20 STATUS_API_KEY= 21 - # The bot's token 22 - TOKEN= 23 # Frontend envs 24 VITE_BOT_API_URL= 25 VITE_STATUS_API_KEY= 26 - VITE_FRONTEND_URL= 27 - VITE_DISCORD_CLIENT_ID= 28 - 29 # Deployment notification webhook 30 DEPLOYMENT_WEBHOOK_URL=
··· 2 ALLOWED_ORIGINS= 3 # Encryption secret for data stored in the Database (Like API keys) 4 API_KEY_ENCRYPTION_SECRET= 5 + # Discord bot credentials, can be found in https://discord.com/developers/applications 6 CLIENT_ID= 7 + CLIENT_SECRET= 8 + REDIRECT_URI=http://localhost:3000/api/auth/discord/callback 9 + TOKEN= 10 # Postgresql database URLs 11 DATABASE_URL= 12 # Openrouter API key for the default AI model 13 OPENROUTER_API_KEY= 14 # OpenWeather API key for the weather command 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 20 # Log level for the application (debug, info, warn, error) 21 LOG_LEVEL=info 22 # Node environment (development, production) ··· 25 SOURCE_COMMIT= 26 # The API key going to be used for the status API 27 STATUS_API_KEY= 28 # Frontend envs 29 VITE_BOT_API_URL= 30 VITE_STATUS_API_KEY= 31 # Deployment notification webhook 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 jobs: 9 pr-checks: 10 name: PR Quality Checks 11 - runs-on: self-hosted 12 permissions: 13 contents: read 14 pull-requests: read ··· 70 71 size-check: 72 name: Bundle Size Check 73 - runs-on: self-hosted 74 permissions: 75 contents: read 76 pull-requests: read
··· 8 jobs: 9 pr-checks: 10 name: PR Quality Checks 11 + runs-on: ubuntu-latest 12 permissions: 13 contents: read 14 pull-requests: read ··· 70 71 size-check: 72 name: Bundle Size Check 73 + runs-on: ubuntu-latest 74 permissions: 75 contents: read 76 pull-requests: read
+2 -10
Dockerfile
··· 3 ARG SOURCE_COMMIT 4 ARG VITE_BOT_API_URL 5 ARG VITE_STATUS_API_KEY 6 - ARG VITE_FRONTEND_URL 7 - ARG VITE_DISCORD_CLIENT_ID 8 ARG STATUS_API_KEY 9 10 ENV SOURCE_COMMIT=${SOURCE_COMMIT} 11 ENV NODE_ENV=production 12 ENV VITE_BOT_API_URL=${VITE_BOT_API_URL} 13 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 ENV STATUS_API_KEY=${STATUS_API_KEY} 17 18 WORKDIR /app 19 20 21 - 22 COPY package.json bun.lock ./ 23 RUN bun install --frozen-lockfile 24 ··· 52 ARG SOURCE_COMMIT 53 ARG VITE_BOT_API_URL 54 ARG VITE_STATUS_API_KEY 55 - ARG VITE_FRONTEND_URL 56 - ARG VITE_DISCORD_CLIENT_ID 57 58 ENV SOURCE_COMMIT=${SOURCE_COMMIT} 59 ENV NODE_ENV=production 60 ENV VITE_BOT_API_URL=${VITE_BOT_API_URL} 61 ENV STATUS_API_KEY=${STATUS_API_KEY} 62 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 66 - RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* && \ 67 groupadd -g 1001 nodejs && \ 68 useradd -r -u 1001 -g nodejs aethel 69
··· 3 ARG SOURCE_COMMIT 4 ARG VITE_BOT_API_URL 5 ARG VITE_STATUS_API_KEY 6 ARG STATUS_API_KEY 7 8 ENV SOURCE_COMMIT=${SOURCE_COMMIT} 9 ENV NODE_ENV=production 10 ENV VITE_BOT_API_URL=${VITE_BOT_API_URL} 11 ENV VITE_STATUS_API_KEY=${VITE_STATUS_API_KEY} 12 ENV STATUS_API_KEY=${STATUS_API_KEY} 13 14 WORKDIR /app 15 16 17 + RUN apt-get update && apt-get install -y git fonts-dejavu-core fontconfig && rm -rf /var/lib/apt/lists/* 18 COPY package.json bun.lock ./ 19 RUN bun install --frozen-lockfile 20 ··· 48 ARG SOURCE_COMMIT 49 ARG VITE_BOT_API_URL 50 ARG VITE_STATUS_API_KEY 51 52 ENV SOURCE_COMMIT=${SOURCE_COMMIT} 53 ENV NODE_ENV=production 54 ENV VITE_BOT_API_URL=${VITE_BOT_API_URL} 55 ENV STATUS_API_KEY=${STATUS_API_KEY} 56 ENV VITE_STATUS_API_KEY=${VITE_STATUS_API_KEY} 57 58 + RUN apt-get update && apt-get install -y curl git fonts-dejavu-core fontconfig && rm -rf /var/lib/apt/lists/* && \ 59 groupadd -g 1001 nodejs && \ 60 useradd -r -u 1001 -g nodejs aethel 61
+1 -6
README.md
··· 51 Run all SQL migrations: 52 53 ```sh 54 - bun run scripts/run-migration.js # or node scripts/run-migration.js 55 ``` 56 57 --- ··· 86 This project is licensed under the MIT License. 87 88 See [LICENSE](LICENSE) for details. 89 - 90 - 7. Start the bot: 91 - ```bash 92 - bun start 93 - ``` 94 95 ## Usage 96
··· 51 Run all SQL migrations: 52 53 ```sh 54 + bun run scripts/run-migrations.js # or node scripts/run-migrations.js 55 ``` 56 57 --- ··· 86 This project is licensed under the MIT License. 87 88 See [LICENSE](LICENSE) for details. 89 90 ## Usage 91
+215 -50
bun.lock
··· 4 "": { 5 "name": "aethel", 6 "dependencies": { 7 - "@atproto/identity": "^0.4.8", 8 - "@discordjs/rest": "^2.5.1", 9 - "@fedify/fedify": "^1.1.0", 10 "@types/he": "^1.2.3", 11 "@types/sanitize-html": "^2.16.0", 12 - "axios": "^1.11.0", 13 - "city-timezones": "^1.3.1", 14 "cors": "^2.8.5", 15 - "discord.js": "^14.21.0", 16 "dotenv": "^16.6.1", 17 "eslint-plugin-prettier": "^5.5.4", 18 "express": "^4.21.2", ··· 24 "moment-timezone": "^0.6.0", 25 "node-fetch": "^3.3.2", 26 "open-graph-scraper": "^6.10.0", 27 - "openai": "^5.12.2", 28 "pg": "^8.16.3", 29 "sanitize-html": "^2.17.0", 30 "uuid": "^11.1.0", ··· 33 "winston": "^3.17.0", 34 }, 35 "devDependencies": { 36 - "@eslint/js": "^9.33.0", 37 "@types/cors": "^2.8.19", 38 "@types/express": "^4.17.23", 39 "@types/jsonwebtoken": "^9.0.10", 40 - "@types/node": "^24.2.1", 41 "@types/open-graph-scraper": "^5.2.3", 42 "@types/pg": "^8.15.5", 43 "@types/uuid": "^10.0.0", 44 - "@types/validator": "^13.15.2", 45 "@types/whois-json": "^2.0.4", 46 - "eslint": "^9.33.0", 47 "eslint-config-prettier": "^10.1.8", 48 - "globals": "^16.3.0", 49 "nodemon": "^3.1.10", 50 "prettier": "^3.6.2", 51 "tsc-alias": "^1.8.16", 52 "tsconfig-paths": "^4.2.0", 53 - "tsx": "^4.20.3", 54 "typescript": "^5.9.2", 55 - "typescript-eslint": "^8.39.0", 56 }, 57 }, 58 }, 59 "packages": { 60 - "@atproto/common-web": ["@atproto/common-web@0.4.2", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw=="], 61 62 "@atproto/crypto": ["@atproto/crypto@0.4.4", "", { "dependencies": { "@noble/curves": "^1.7.0", "@noble/hashes": "^1.6.1", "uint8arrays": "3.0.0" } }, "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA=="], 63 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=="], 65 66 "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], 67 ··· 77 78 "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], 79 80 - "@discordjs/rest": ["@discordjs/rest@2.5.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw=="], 81 82 "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 83 ··· 135 136 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], 137 138 - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], 139 140 "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], 141 ··· 147 148 "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], 149 150 - "@eslint/js": ["@eslint/js@9.34.0", "", {}, "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw=="], 151 152 "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], 153 ··· 155 156 "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], 157 158 - "@fedify/fedify": ["@fedify/fedify@1.8.8", "", { "dependencies": { "@cfworker/json-schema": "^4.1.1", "@hugoalh/http-header-link": "^1.0.2", "@js-temporal/polyfill": "^0.5.1", "@logtape/logtape": "^1.0.0", "@multiformats/base-x": "^4.0.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@phensley/language-tag": "^1.9.0", "asn1js": "^3.0.5", "byte-encodings": "^1.0.11", "es-toolkit": "^1.39.5", "json-canon": "^1.0.1", "jsonld": "^8.3.2", "multicodec": "^3.2.1", "pkijs": "^3.2.4", "structured-field-values": "^2.0.4", "uri-template-router": "^0.0.17", "url-template": "^3.1.1", "urlpattern-polyfill": "^10.1.0" } }, "sha512-1w5YfKbh8wNbcJ1s0Ttb2l3i4oiWmQ7z5sbKqQJMhBW6odaGGVgEO2bl6ZI5Tg0i5o/LK3ODOKLTlr8F1wibHA=="], 159 160 "@hugoalh/http-header-link": ["@hugoalh/http-header-link@1.0.3", "", { "dependencies": { "@hugoalh/is-string-singleline": "^1.0.4" } }, "sha512-x4jzzKSzZQY115H/GxUWaAHzT5eqLXt99uSKY7+0O/h3XrV248+CkZA7cA274QahXzWkGQYYug/AF6QUkTnLEw=="], 161 ··· 173 174 "@logtape/logtape": ["@logtape/logtape@1.0.4", "", {}, "sha512-YvNVrXIxVpnY528zoiEjX8PqTfr0UCtKXyssvaWL8AE+OByFTCooKuKMdPlm6g65YUI9fPXrHn4UnogSskABnA=="], 175 176 "@multiformats/base-x": ["@multiformats/base-x@4.0.1", "", {}, "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw=="], 177 178 "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], ··· 223 224 "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 225 226 - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], 227 228 "@types/open-graph-scraper": ["@types/open-graph-scraper@5.2.3", "", { "dependencies": { "open-graph-scraper": "*" } }, "sha512-R6ew1HJndBKsys2+Y10VW8yy3ojS7eF/mFXrOZSFxVqY7WI4ubxaFvgfaULnRn2pq149SpS2GZNB9i9Y5fQqEw=="], 229 ··· 243 244 "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], 245 246 - "@types/validator": ["@types/validator@13.15.2", "", {}, "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q=="], 247 248 "@types/whois-json": ["@types/whois-json@2.0.4", "", {}, "sha512-Pp5N/+A6LUE0FWXz6wQ2gV5wEw0uEqFBeSLuQAGdeTyRJv/bbz7PPj3H78jyulvQu7cnMpXTzKx4bo8TuPAYhw=="], 249 250 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 251 252 - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/utils": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw=="], 253 254 - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg=="], 255 256 - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew=="], 257 258 - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="], 259 260 - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ=="], 261 262 - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q=="], 263 264 - "@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], 265 266 - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw=="], 267 268 - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="], 269 270 - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA=="], 271 272 "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], 273 ··· 299 300 "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], 301 302 - "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], 303 304 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 305 306 "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], 307 308 "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 310 "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], ··· 313 314 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 315 316 "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], 317 318 "byte-encodings": ["byte-encodings@1.0.11", "", {}, "sha512-+/xR2+ySc2yKGtud3DGkGSH1DNwHfRVK0KTnMhoeH36/KwG+tHQ4d9B3jxJFq7dW27YcfudkywaYJRPA2dmxzg=="], 319 ··· 333 334 "canonicalize": ["canonicalize@1.0.8", "", {}, "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A=="], 335 336 "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 337 338 "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 346 "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], 347 348 - "city-timezones": ["city-timezones@1.3.1", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-YCeJKGyw3DA+wV/oyuFuJlk4oqN9zkfLP+fz2nEXUBm9sW1xZaXQsKQoc8l8hP+vI45GPOq8OuGrlGXUcnLISA=="], 349 350 - "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], 351 352 "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], 353 ··· 365 366 "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 367 368 "constant-case": ["constant-case@2.0.0", "", { "dependencies": { "snake-case": "^2.1.0", "upper-case": "^1.1.1" } }, "sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ=="], 369 370 "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], ··· 377 378 "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], 379 380 "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 382 "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 384 "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], 385 386 "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], 387 388 "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 389 390 "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], 391 392 "dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="], 393 394 "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 395 ··· 401 402 "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], 403 404 "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], 405 406 "discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 407 408 - "discord.js": ["discord.js@14.21.0", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-U5w41cEmcnSfwKYlLv5RJjB8Joa+QJyRwIJz5i/eg+v2Qvv6EYpCRhN9I2Rlf0900LuqSDg8edakUATrDZQncQ=="], 409 410 "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], 411 ··· 433 434 "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 436 "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 437 438 "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], ··· 445 446 "es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="], 447 448 "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=="], 449 450 "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 451 452 "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 453 454 - "eslint": ["eslint@9.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg=="], 455 456 "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], 457 ··· 460 "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], 461 462 "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 463 464 "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 ··· 473 474 "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 475 476 "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], 477 478 "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 480 "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], 481 482 "express-validator": ["express-validator@7.2.1", "", { "dependencies": { "lodash": "^4.17.21", "validator": "~13.12.0" } }, "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ=="], 483 484 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 485 ··· 500 "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 501 502 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 503 504 "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 ··· 521 522 "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], 523 524 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 525 526 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], ··· 533 534 "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], 535 536 "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 537 538 - "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], 539 540 "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], 541 ··· 564 "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 566 "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], 567 568 "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 569 ··· 575 576 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 577 578 "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], 579 580 "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], ··· 596 "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], 597 598 "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], 599 600 "is-upper-case": ["is-upper-case@1.1.2", "", { "dependencies": { "upper-case": "^1.1.0" } }, "sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw=="], 601 ··· 685 686 "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 687 688 "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 689 690 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 691 692 "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], 693 ··· 703 704 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 705 706 "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 707 708 "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], 709 710 "no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="], 711 712 "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], 713 714 "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 716 "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 718 "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], ··· 725 726 "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 727 728 "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], 729 730 "open-graph-scraper": ["open-graph-scraper@6.10.0", "", { "dependencies": { "chardet": "^2.1.0", "cheerio": "^1.0.0-rc.12", "iconv-lite": "^0.6.3", "undici": "^6.21.2" } }, "sha512-JTuaO/mWUPduYCIQvunmsQnfGpSRFUTEh4k5cW2KOafJxTm3Z99z25/c1oO9QnIh2DK7ol5plJAq3EUVy+5xyw=="], 731 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=="], 733 734 "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 735 ··· 799 800 "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], 801 802 "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 803 804 "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], ··· 811 812 "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], 813 814 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 815 816 "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], ··· 819 820 "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], 821 822 "queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="], 823 824 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], ··· 827 828 "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 830 "rdf-canonize": ["rdf-canonize@3.4.0", "", { "dependencies": { "setimmediate": "^1.0.5" } }, "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA=="], 831 832 "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 "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 845 846 "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 847 848 "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 849 ··· 871 872 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 873 874 "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 876 "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 "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 880 "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=="], 881 882 "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], 883 ··· 893 894 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 895 896 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 897 898 "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], ··· 900 "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], 901 902 "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 903 904 "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 ··· 919 920 "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], 921 922 "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], 923 924 "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 "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 929 930 "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="], 931 932 "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], 933 ··· 941 942 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 943 944 - "tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="], 945 946 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 947 948 "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], 949 950 "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 951 952 - "typescript-eslint": ["typescript-eslint@8.39.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.0", "@typescript-eslint/parser": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q=="], 953 954 "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 955 ··· 959 960 "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], 961 962 - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 963 964 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 965 ··· 975 976 "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], 977 978 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 979 980 "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], ··· 989 990 "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], 991 992 "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], 993 994 "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], 995 996 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 997 ··· 1007 1008 "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 1009 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=="], 1011 1012 "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 1014 "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], 1015 1016 - "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], 1017 1018 "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], 1019 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=="], 1021 1022 - "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], 1023 1024 "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 1025 ··· 1027 1028 "@digitalbazaar/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], 1029 1030 "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 1031 1032 "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], ··· 1069 1070 "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], 1071 1072 "discord.js/@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], 1073 1074 "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], ··· 1083 1084 "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 1085 1086 "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 1087 1088 "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1089 1090 - "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], 1091 1092 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1093 ··· 1099 1100 "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], 1101 1102 "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1103 1104 "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1105 1106 "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1107 1108 - "yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], 1109 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=="], 1111 1112 - "yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], 1113 } 1114 }
··· 4 "": { 5 "name": "aethel", 6 "dependencies": { 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", 11 "@types/he": "^1.2.3", 12 "@types/sanitize-html": "^2.16.0", 13 + "axios": "^1.12.2", 14 + "canvas": "^3.2.0", 15 + "city-timezones": "^1.3.2", 16 + "concurrently": "^9.2.1", 17 "cors": "^2.8.5", 18 + "discord.js": "^14.22.1", 19 "dotenv": "^16.6.1", 20 "eslint-plugin-prettier": "^5.5.4", 21 "express": "^4.21.2", ··· 27 "moment-timezone": "^0.6.0", 28 "node-fetch": "^3.3.2", 29 "open-graph-scraper": "^6.10.0", 30 + "openai": "^5.23.1", 31 "pg": "^8.16.3", 32 "sanitize-html": "^2.17.0", 33 "uuid": "^11.1.0", ··· 36 "winston": "^3.17.0", 37 }, 38 "devDependencies": { 39 + "@eslint/js": "^9.36.0", 40 "@types/cors": "^2.8.19", 41 "@types/express": "^4.17.23", 42 "@types/jsonwebtoken": "^9.0.10", 43 + "@types/node": "^24.5.2", 44 "@types/open-graph-scraper": "^5.2.3", 45 "@types/pg": "^8.15.5", 46 "@types/uuid": "^10.0.0", 47 + "@types/validator": "^13.15.3", 48 "@types/whois-json": "^2.0.4", 49 + "eslint": "^9.36.0", 50 "eslint-config-prettier": "^10.1.8", 51 + "globals": "^16.4.0", 52 "nodemon": "^3.1.10", 53 "prettier": "^3.6.2", 54 "tsc-alias": "^1.8.16", 55 "tsconfig-paths": "^4.2.0", 56 + "tsx": "^4.20.6", 57 "typescript": "^5.9.2", 58 + "typescript-eslint": "^8.44.1", 59 }, 60 }, 61 }, 62 "packages": { 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=="], 64 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=="], 66 67 + "@atproto/identity": ["@atproto/identity@0.4.9", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/crypto": "^0.4.4" } }, "sha512-pRYCaeaEJMZ4vQlRQYYTrF3cMiRp21n/k/pUT1o7dgKby56zuLErDmFXkbKfKWPf7SgWRgamSaNmsGLqAOD7lQ=="], 68 69 "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], 70 ··· 80 81 "@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="], 82 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=="], 84 85 "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="], 86 ··· 138 139 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="], 140 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=="], 142 143 "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], 144 ··· 150 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=="], 152 153 + "@eslint/js": ["@eslint/js@9.36.0", "", {}, "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw=="], 154 155 "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], 156 ··· 158 159 "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], 160 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=="], 162 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=="], 164 ··· 176 177 "@logtape/logtape": ["@logtape/logtape@1.0.4", "", {}, "sha512-YvNVrXIxVpnY528zoiEjX8PqTfr0UCtKXyssvaWL8AE+OByFTCooKuKMdPlm6g65YUI9fPXrHn4UnogSskABnA=="], 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 + 181 "@multiformats/base-x": ["@multiformats/base-x@4.0.1", "", {}, "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw=="], 182 183 "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], ··· 228 229 "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 230 231 + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], 232 233 "@types/open-graph-scraper": ["@types/open-graph-scraper@5.2.3", "", { "dependencies": { "open-graph-scraper": "*" } }, "sha512-R6ew1HJndBKsys2+Y10VW8yy3ojS7eF/mFXrOZSFxVqY7WI4ubxaFvgfaULnRn2pq149SpS2GZNB9i9Y5fQqEw=="], 234 ··· 248 249 "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], 250 251 + "@types/validator": ["@types/validator@13.15.3", "", {}, "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q=="], 252 253 "@types/whois-json": ["@types/whois-json@2.0.4", "", {}, "sha512-Pp5N/+A6LUE0FWXz6wQ2gV5wEw0uEqFBeSLuQAGdeTyRJv/bbz7PPj3H78jyulvQu7cnMpXTzKx4bo8TuPAYhw=="], 254 255 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 256 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=="], 258 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=="], 260 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=="], 262 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=="], 264 265 + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ=="], 266 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=="], 268 269 + "@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], 270 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=="], 272 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=="], 274 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=="], 276 277 "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="], 278 ··· 304 305 "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], 306 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=="], 308 309 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 310 + 311 + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 312 313 "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], 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 + 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=="], 318 319 "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], ··· 322 323 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 324 325 + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], 326 + 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=="], 330 331 "byte-encodings": ["byte-encodings@1.0.11", "", {}, "sha512-+/xR2+ySc2yKGtud3DGkGSH1DNwHfRVK0KTnMhoeH36/KwG+tHQ4d9B3jxJFq7dW27YcfudkywaYJRPA2dmxzg=="], 332 ··· 346 347 "canonicalize": ["canonicalize@1.0.8", "", {}, "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A=="], 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 + 351 "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 352 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=="], ··· 360 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=="], 362 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=="], 366 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=="], 368 369 "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], 370 ··· 382 383 "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 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 + 387 "constant-case": ["constant-case@2.0.0", "", { "dependencies": { "snake-case": "^2.1.0", "upper-case": "^1.1.1" } }, "sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ=="], 388 389 "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], ··· 396 397 "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], 398 399 + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], 400 + 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=="], 402 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=="], 404 405 "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], 406 407 + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], 408 + 409 "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], 410 411 "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 412 413 "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], 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 + 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=="], 422 423 "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 424 ··· 430 431 "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], 432 433 + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 434 + 435 "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], 436 437 "discord-api-types": ["discord-api-types@0.38.18", "", {}, "sha512-ygenySjZKUaBf5JT8BNhZSxLzwpwdp41O0wVroOTu/N2DxFH7dxYTZUSnFJ6v+/2F3BMcnD47PC47u4aLOLxrQ=="], 438 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=="], 440 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=="], 442 ··· 464 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=="], 466 467 + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], 468 + 469 "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 470 471 "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], ··· 478 479 "es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="], 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 + 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=="], 490 491 "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 492 493 "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 494 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=="], 496 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=="], 498 ··· 501 "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], 502 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=="], 506 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=="], 508 ··· 516 517 "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 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 + 521 "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], 522 523 + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], 524 + 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=="], 526 527 "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], 528 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=="], 532 533 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 534 ··· 549 "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 550 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=="], 554 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=="], 556 ··· 572 573 "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], 574 575 + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], 576 + 577 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 578 579 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], ··· 586 587 "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], 588 589 + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], 590 + 591 "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 592 593 + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], 594 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=="], 596 ··· 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=="], 620 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=="], 624 625 "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 626 ··· 632 633 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 634 635 + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], 636 + 637 "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], 638 639 "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], ··· 655 "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], 656 657 "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], 658 + 659 + "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], 660 661 "is-upper-case": ["is-upper-case@1.1.2", "", { "dependencies": { "upper-case": "^1.1.0" } }, "sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw=="], 662 ··· 746 747 "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 748 749 + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], 750 + 751 "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 752 753 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 754 + 755 + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], 756 757 "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], 758 ··· 768 769 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 770 771 + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], 772 + 773 "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 774 775 "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], 776 777 + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], 778 + 779 "no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="], 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 + 785 "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], 786 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=="], 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 + 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=="], 792 793 "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], ··· 800 801 "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 802 803 + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 804 + 805 "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], 806 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=="], 808 809 + "openai": ["openai@5.23.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-APxMtm5mln4jhKhAr0d5zP9lNsClx4QwJtg8RUvYSSyxYCTHLNJnLEcSHbJ6t0ori8Pbr9HZGfcPJ7LEy73rvQ=="], 810 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=="], 812 ··· 876 877 "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], 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 + 881 "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 882 883 "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], ··· 890 891 "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], 892 893 + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], 894 + 895 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 896 897 "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], ··· 900 901 "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], 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 + 905 "queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="], 906 907 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], ··· 910 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=="], 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 + 915 "rdf-canonize": ["rdf-canonize@3.4.0", "", { "dependencies": { "setimmediate": "^1.0.5" } }, "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA=="], 916 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=="], ··· 929 "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 930 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=="], 934 935 "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 936 ··· 958 959 "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 960 961 + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], 962 + 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=="], 964 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=="], ··· 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=="], 968 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=="], 974 975 "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], 976 ··· 986 987 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 988 989 + "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], 990 + 991 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 992 993 "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], ··· 995 "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], 996 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=="], 1000 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=="], 1002 ··· 1016 1017 "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], 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 + 1023 "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], 1024 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=="], ··· 1029 "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 1030 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=="], 1036 1037 "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], 1038 ··· 1046 1047 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1048 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=="], 1054 1055 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 1056 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=="], 1060 1061 "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 1062 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=="], 1064 1065 "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 1066 ··· 1070 1071 "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], 1072 1073 + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], 1074 1075 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 1076 ··· 1086 1087 "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], 1088 1089 + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], 1090 + 1091 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 1092 1093 "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], ··· 1102 1103 "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], 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 + 1109 "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], 1110 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=="], 1114 1115 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1116 ··· 1126 1127 "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 1128 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=="], 1132 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=="], 1134 1135 "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], 1136 1137 + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 1138 + 1139 + "yaeti": ["yaeti@0.0.6", "", {}, "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug=="], 1140 1141 "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], 1142 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=="], 1144 1145 + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], 1146 1147 "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 1148 ··· 1150 1151 "@digitalbazaar/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], 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 + 1155 "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 1156 1157 "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], ··· 1194 1195 "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], 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 + 1201 "discord.js/@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], 1202 1203 "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], ··· 1212 1213 "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 1214 1215 + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], 1216 + 1217 "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 1218 1219 "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1220 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=="], 1242 1243 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1244 ··· 1250 1251 "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], 1252 1253 + "concurrently/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 1254 + 1255 "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1256 1257 "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1258 1259 "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1260 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=="], 1264 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=="], 1266 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=="], 1278 } 1279 }
+2 -2
docker-compose.example.yml
··· 6 args: 7 - VITE_BOT_API_URL=${VITE_BOT_API_URL:-https://aethel.xyz} 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 - STATUS_API_KEY=${STATUS_API_KEY} 12 - SOURCE_COMMIT=${SOURCE_COMMIT:-development} 13 container_name: aethel-bot ··· 18 NODE_ENV: production 19 TOKEN: ${TOKEN} 20 CLIENT_ID: ${CLIENT_ID} 21 DATABASE_URL: ${DATABASE_URL} 22 API_KEY_ENCRYPTION_SECRET: ${API_KEY_ENCRYPTION_SECRET} 23 STATUS_API_KEY: ${STATUS_API_KEY}
··· 6 args: 7 - VITE_BOT_API_URL=${VITE_BOT_API_URL:-https://aethel.xyz} 8 - VITE_STATUS_API_KEY=${VITE_STATUS_API_KEY} 9 - STATUS_API_KEY=${STATUS_API_KEY} 10 - SOURCE_COMMIT=${SOURCE_COMMIT:-development} 11 container_name: aethel-bot ··· 16 NODE_ENV: production 17 TOKEN: ${TOKEN} 18 CLIENT_ID: ${CLIENT_ID} 19 + CLIENT_SECRET: ${CLIENT_SECRET} 20 + REDIRECT_URI: ${REDIRECT_URI} 21 DATABASE_URL: ${DATABASE_URL} 22 API_KEY_ENCRYPTION_SECRET: ${API_KEY_ENCRYPTION_SECRET} 23 STATUS_API_KEY: ${STATUS_API_KEY}
+4
environment.d.ts
··· 6 DATABASE_URL: string; 7 OPENROUTER_API_KEY: string; 8 OPENWEATHER_API_KEY: string; 9 SOURCE_COMMIT: string; 10 STATUS_API_KEY: string; 11 TOKEN: string; 12 CLIENT_ID: string; 13 } 14 } 15 }
··· 6 DATABASE_URL: string; 7 OPENROUTER_API_KEY: string; 8 OPENWEATHER_API_KEY: string; 9 + MASSIVE_API_KEY: string; 10 + MASSIVE_API_BASE_URL: string; 11 SOURCE_COMMIT: string; 12 STATUS_API_KEY: string; 13 TOKEN: string; 14 CLIENT_ID: string; 15 + CLIENT_SECRET: string; 16 + REDIRECT_URI: string; 17 } 18 } 19 }
+2 -2
locales/de.json
··· 153 "modal": { 154 "title": "Gib deine API-Zugangsdaten ein", 155 "apikey": "API-Schlรผssel", 156 - "apikeyplaceholder": "Um deinen Schlรผssel nicht mehr zu verwenden: /ai use_custom_api false", 157 "apiurl": "API-URL", 158 "apiurlplaceholder": "Deine API-URL", 159 "model": "Modell", 160 "modelplaceholder": "Modellname (z.B. gpt-4)" 161 }, 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`", 164 "process": { 165 "dailylimit": "Du hast dein tรคgliches Limit an KI-Anfragen erreicht", 166 "noapikey": "Bitte richte zuerst deinen API-Schlรผssel ein"
··· 153 "modal": { 154 "title": "Gib deine API-Zugangsdaten ein", 155 "apikey": "API-Schlรผssel", 156 + "apikeyplaceholder": "Um deinen Schlรผssel nicht mehr zu verwenden: /ai custom_setup:false", 157 "apiurl": "API-URL", 158 "apiurlplaceholder": "Deine API-URL", 159 "model": "Modell", 160 "modelplaceholder": "Modellname (z.B. gpt-4)" 161 }, 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 custom_setup:false`", 164 "process": { 165 "dailylimit": "Du hast dein tรคgliches Limit an KI-Anfragen erreicht", 166 "noapikey": "Bitte richte zuerst deinen API-Schlรผssel ein"
+52 -3
locales/en-US.json
··· 51 "newcat": "New cat", 52 "error": "Sorry, I had trouble fetching a cat image. Please try again later!" 53 }, 54 "dog": { 55 "name": "dog", 56 "description": "Get a random dog image!", ··· 80 "nolocation": "Location not found. Please check the city name and try again.", 81 "apikeymissing": "OpenWeather API key is missing or invalid." 82 }, 83 "joke": { 84 "name": "joke", 85 "description": "Get a random joke!", ··· 162 "modal": { 163 "title": "Enter your API Credentials", 164 "apikey": "API Key", 165 - "apikeyplaceholder": "To stop using your key: /ai use_custom_api false", 166 "apiurl": "API Url", 167 "apiurlplaceholder": "Your API Url", 168 "model": "Model", 169 "modelplaceholder": "Model name (eg. gpt-4)" 170 }, 171 "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`", 173 "testing": "Testing API key...", 174 "testfailed": "โŒ API key test failed: {error}. Please check your credentials and try again.", 175 "testsuccess": "โœ… API key test successful! Credentials saved.", 176 "process": { 177 - "dailylimit": "You've reached your daily limit of AI requests", 178 "noapikey": "Please set up your API key first" 179 }, 180 "errors": {
··· 51 "newcat": "New cat", 52 "error": "Sorry, I had trouble fetching a cat image. Please try again later!" 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 + }, 66 "dog": { 67 "name": "dog", 68 "description": "Get a random dog image!", ··· 92 "nolocation": "Location not found. Please check the city name and try again.", 93 "apikeymissing": "OpenWeather API key is missing or invalid." 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 + }, 132 "joke": { 133 "name": "joke", 134 "description": "Get a random joke!", ··· 211 "modal": { 212 "title": "Enter your API Credentials", 213 "apikey": "API Key", 214 + "apikeyplaceholder": "To stop using your key: /ai custom_setup:false", 215 "apiurl": "API Url", 216 "apiurlplaceholder": "Your API Url", 217 "model": "Model", 218 "modelplaceholder": "Model name (eg. gpt-4)" 219 }, 220 "nopendingrequest": "No pending request found. Please try the command again.", 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`", 222 "testing": "Testing API key...", 223 "testfailed": "โŒ API key test failed: {error}. Please check your credentials and try again.", 224 "testsuccess": "โœ… API key test successful! Credentials saved.", 225 "process": { 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.", 227 "noapikey": "Please set up your API key first" 228 }, 229 "errors": {
+2 -2
locales/es-419.json
··· 153 "modal": { 154 "title": "Ingresa tus credenciales de API", 155 "apikey": "Clave API", 156 - "apikeyplaceholder": "Para dejar de usar tu clave: /ai use_custom_api false", 157 "apiurl": "URL de la API", 158 "apiurlplaceholder": "Tu URL de API", 159 "model": "Modelo", 160 "modelplaceholder": "Nombre del modelo (ej. gpt-4)" 161 }, 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`", 164 "process": { 165 "dailylimit": "Has alcanzado tu lรญmite diario de solicitudes de IA", 166 "noapikey": "Por favor, configura primero tu clave API"
··· 153 "modal": { 154 "title": "Ingresa tus credenciales de API", 155 "apikey": "Clave API", 156 + "apikeyplaceholder": "Para dejar de usar tu clave: /ai custom_setup:false", 157 "apiurl": "URL de la API", 158 "apiurlplaceholder": "Tu URL de API", 159 "model": "Modelo", 160 "modelplaceholder": "Nombre del modelo (ej. gpt-4)" 161 }, 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 custom_setup:false`", 164 "process": { 165 "dailylimit": "Has alcanzado tu lรญmite diario de solicitudes de IA", 166 "noapikey": "Por favor, configura primero tu clave API"
+2 -2
locales/es-ES.json
··· 153 "modal": { 154 "title": "Introduce tus credenciales de API", 155 "apikey": "Clave API", 156 - "apikeyplaceholder": "Para dejar de usar tu clave: /ai use_custom_api false", 157 "apiurl": "URL de la API", 158 "apiurlplaceholder": "Tu URL de API", 159 "model": "Modelo", 160 "modelplaceholder": "Nombre del modelo (ej. gpt-4)" 161 }, 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`", 164 "process": { 165 "dailylimit": "Has alcanzado tu lรญmite diario de solicitudes de IA", 166 "noapikey": "Por favor, configura primero tu clave API"
··· 153 "modal": { 154 "title": "Introduce tus credenciales de API", 155 "apikey": "Clave API", 156 + "apikeyplaceholder": "Para dejar de usar tu clave: /ai custom_setup:false", 157 "apiurl": "URL de la API", 158 "apiurlplaceholder": "Tu URL de API", 159 "model": "Modelo", 160 "modelplaceholder": "Nombre del modelo (ej. gpt-4)" 161 }, 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 custom_setup:false`", 164 "process": { 165 "dailylimit": "Has alcanzado tu lรญmite diario de solicitudes de IA", 166 "noapikey": "Por favor, configura primero tu clave API"
+2 -2
locales/fr.json
··· 153 "modal": { 154 "title": "Entrez vos identifiants API", 155 "apikey": "Clรฉ API", 156 - "apikeyplaceholder": "Pour ne plus utiliser votre clรฉ : /ai use_custom_api false", 157 "apiurl": "URL de l'API", 158 "apiurlplaceholder": "Votre URL d'API", 159 "model": "Modรจle", 160 "modelplaceholder": "Nom du modรจle (ex. gpt-4)" 161 }, 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`", 164 "process": { 165 "dailylimit": "Vous avez atteint votre limite quotidienne de requรชtes IA", 166 "noapikey": "Veuillez d'abord configurer votre clรฉ API"
··· 153 "modal": { 154 "title": "Entrez vos identifiants API", 155 "apikey": "Clรฉ API", 156 + "apikeyplaceholder": "Pour ne plus utiliser votre clรฉ : /ai custom_setup:false", 157 "apiurl": "URL de l'API", 158 "apiurlplaceholder": "Votre URL d'API", 159 "model": "Modรจle", 160 "modelplaceholder": "Nom du modรจle (ex. gpt-4)" 161 }, 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 custom_setup:false`", 164 "process": { 165 "dailylimit": "Vous avez atteint votre limite quotidienne de requรชtes IA", 166 "noapikey": "Veuillez d'abord configurer votre clรฉ API"
+4 -4
locales/ja.json
··· 153 "modal": { 154 "title": "API่ช่จผๆƒ…ๅ ฑใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„", 155 "apikey": "APIใ‚ญใƒผ", 156 - "apikeyplaceholder": "ใ‚ญใƒผใฎไฝฟ็”จใ‚’ใ‚„ใ‚ใ‚‹ใซใฏ: /ai use_custom_api false", 157 - "apiurl": "APIใฎURL", 158 "apiurlplaceholder": "ใ‚ใชใŸใฎAPIใฎURL", 159 "model": "ใƒขใƒ‡ใƒซ", 160 - "modelplaceholder": "ใƒขใƒ‡ใƒซๅ๏ผˆไพ‹: gpt-4๏ผ‰" 161 }, 162 "nopendingrequest": "ไฟ็•™ไธญใฎใƒชใ‚ฏใ‚จใ‚นใƒˆใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚ใ‚‚ใ†ไธ€ๅบฆใ‚ณใƒžใƒณใƒ‰ใ‚’ใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚", 163 - "apicredssaved": "API่ช่จผๆƒ…ๅ ฑใŒไฟๅญ˜ใ•ใ‚Œใพใ—ใŸใ€‚ใ“ใ‚Œใงๅ†ๅ…ฅๅŠ›ใ›ใšใซ`/ai`ใ‚ณใƒžใƒณใƒ‰ใ‚’ไฝฟใˆใพใ™ใ€‚ใ‚ญใƒผใฎไฝฟ็”จใ‚’ใ‚„ใ‚ใ‚‹ใซใฏ `/ai use_custom_api false` ใ‚’ไฝฟใฃใฆใใ ใ•ใ„", 164 "process": { 165 "dailylimit": "AIใƒชใ‚ฏใ‚จใ‚นใƒˆใฎ1ๆ—ฅไธŠ้™ใซ้”ใ—ใพใ—ใŸ", 166 "noapikey": "ใพใšAPIใ‚ญใƒผใ‚’่จญๅฎšใ—ใฆใใ ใ•ใ„"
··· 153 "modal": { 154 "title": "API่ช่จผๆƒ…ๅ ฑใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„", 155 "apikey": "APIใ‚ญใƒผ", 156 + "apikeyplaceholder": "ใ‚ญใƒผใฎไฝฟ็”จใ‚’ใ‚„ใ‚ใ‚‹ใซใฏ: /ai custom_setup:false", 157 "apiurlplaceholder": "ใ‚ใชใŸใฎAPIใฎURL", 158 "model": "ใƒขใƒ‡ใƒซ", 159 + "modelplaceholder": "ใƒขใƒ‡ใƒซๅ๏ผˆไพ‹: gpt-4)", 160 + "customsetup": "ใ‚ซใ‚นใ‚ฟใƒ ่จญๅฎš" 161 }, 162 "nopendingrequest": "ไฟ็•™ไธญใฎใƒชใ‚ฏใ‚จใ‚นใƒˆใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚ใ‚‚ใ†ไธ€ๅบฆใ‚ณใƒžใƒณใƒ‰ใ‚’ใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚", 163 + "apicredssaved": "API่ช่จผๆƒ…ๅ ฑใŒไฟๅญ˜ใ•ใ‚Œใพใ—ใŸใ€‚ใ“ใ‚Œใง่ช่จผๆƒ…ๅ ฑใ‚’ๅ†ๅ…ฅๅŠ›ใ™ใ‚‹ใ“ใจใชใ`/ai`ใ‚ณใƒžใƒณใƒ‰ใ‚’ไฝฟ็”จใงใใพใ™ใ€‚ใ‚ญใƒผใฎไฝฟ็”จใ‚’ใ‚„ใ‚ใ‚‹ใซใฏใ€`/ai custom_setup:false`ใ‚’ๅฎŸ่กŒใ—ใฆใใ ใ•ใ„", 164 "process": { 165 "dailylimit": "AIใƒชใ‚ฏใ‚จใ‚นใƒˆใฎ1ๆ—ฅไธŠ้™ใซ้”ใ—ใพใ—ใŸ", 166 "noapikey": "ใพใšAPIใ‚ญใƒผใ‚’่จญๅฎšใ—ใฆใใ ใ•ใ„"
+2 -2
locales/pt-BR.json
··· 153 "modal": { 154 "title": "Insira suas credenciais de API", 155 "apikey": "Chave de API", 156 - "apikeyplaceholder": "Para parar de usar sua chave: /ai use_custom_api false", 157 "apiurl": "URL da API", 158 "apiurlplaceholder": "Sua URL de API", 159 "model": "Modelo", 160 "modelplaceholder": "Nome do modelo (ex. gpt-4)" 161 }, 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`", 164 "process": { 165 "dailylimit": "Vocรช atingiu seu limite diรกrio de solicitaรงรตes de IA", 166 "noapikey": "Por favor, configure sua chave de API primeiro"
··· 153 "modal": { 154 "title": "Insira suas credenciais de API", 155 "apikey": "Chave de API", 156 + "apikeyplaceholder": "Para parar de usar sua chave: /ai custom_setup:false", 157 "apiurl": "URL da API", 158 "apiurlplaceholder": "Sua URL de API", 159 "model": "Modelo", 160 "modelplaceholder": "Nome do modelo (ex. gpt-4)" 161 }, 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 custom_setup:false`", 164 "process": { 165 "dailylimit": "Vocรช atingiu seu limite diรกrio de solicitaรงรตes de IA", 166 "noapikey": "Por favor, configure sua chave de API primeiro"
+2 -2
locales/tr.json
··· 153 "modal": { 154 "title": "API Kimlik Bilgilerinizi Girin", 155 "apikey": "API Anahtarฤฑ", 156 - "apikeyplaceholder": "Kendi anahtarฤฑnฤฑzฤฑ kullanmayฤฑ bฤฑrakmak iรงin: /ai use_custom_api false", 157 "apiurl": "API Adresi", 158 "apiurlplaceholder": "API Adresiniz", 159 "model": "Model", 160 "modelplaceholder": "Model adฤฑ (รถrn. gpt-4)" 161 }, 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", 164 "process": { 165 "dailylimit": "Gรผnlรผk AI istek limitinize ulaลŸtฤฑnฤฑz", 166 "noapikey": "Lรผtfen รถnce API anahtarฤฑnฤฑzฤฑ ayarlayฤฑn"
··· 153 "modal": { 154 "title": "API Kimlik Bilgilerinizi Girin", 155 "apikey": "API Anahtarฤฑ", 156 + "apikeyplaceholder": "Kendi anahtarฤฑnฤฑzฤฑ kullanmayฤฑ bฤฑrakmak iรงin: /ai custom_setup:false", 157 "apiurl": "API Adresi", 158 "apiurlplaceholder": "API Adresiniz", 159 "model": "Model", 160 "modelplaceholder": "Model adฤฑ (รถrn. gpt-4)" 161 }, 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 custom_setup:false yazฤฑn", 164 "process": { 165 "dailylimit": "Gรผnlรผk AI istek limitinize ulaลŸtฤฑnฤฑz", 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 { 2 "name": "aethel", 3 - "version": "2.0.1", 4 "description": "A privacy-conscious, production-ready Discord user bot", 5 "type": "module", 6 "main": "dist/index.js", 7 "scripts": { 8 - "start": "node ./dist/index.js", 9 "dev": "tsx watch src/index.ts", 10 "build": "tsc && node scripts/fix-imports.js", 11 "migrate": "node scripts/run-migrations.js", ··· 15 "lint:format": "eslint ./src ./web/src --ext .ts,.tsx --config eslint.config.cjs --format=codeframe", 16 "format": "prettier --write \"**/*.{js,json,md,ts,tsx}\" --ignore-path .prettierignore", 17 "format:check": "prettier --check \"**/*.{js,json,md,ts,tsx}\" --ignore-path .prettierignore", 18 - "check": "pnpm run lint && pnpm run format:check" 19 }, 20 "dependencies": { 21 - "@atproto/identity": "^0.4.8", 22 - "@discordjs/rest": "^2.5.1", 23 - "@fedify/fedify": "^1.1.0", 24 "@types/he": "^1.2.3", 25 "@types/sanitize-html": "^2.16.0", 26 - "axios": "^1.11.0", 27 - "city-timezones": "^1.3.1", 28 "cors": "^2.8.5", 29 - "discord.js": "^14.21.0", 30 "dotenv": "^16.6.1", 31 "eslint-plugin-prettier": "^5.5.4", 32 "express": "^4.21.2", ··· 38 "moment-timezone": "^0.6.0", 39 "node-fetch": "^3.3.2", 40 "open-graph-scraper": "^6.10.0", 41 - "openai": "^5.12.2", 42 "pg": "^8.16.3", 43 "sanitize-html": "^2.17.0", 44 "uuid": "^11.1.0", ··· 47 "winston": "^3.17.0" 48 }, 49 "devDependencies": { 50 - "@eslint/js": "^9.33.0", 51 "@types/cors": "^2.8.19", 52 "@types/express": "^4.17.23", 53 "@types/jsonwebtoken": "^9.0.10", 54 - "@types/node": "^24.2.1", 55 "@types/open-graph-scraper": "^5.2.3", 56 "@types/pg": "^8.15.5", 57 "@types/uuid": "^10.0.0", 58 - "@types/validator": "^13.15.2", 59 "@types/whois-json": "^2.0.4", 60 - "eslint": "^9.33.0", 61 "eslint-config-prettier": "^10.1.8", 62 - "globals": "^16.3.0", 63 "nodemon": "^3.1.10", 64 "prettier": "^3.6.2", 65 "tsc-alias": "^1.8.16", 66 "tsconfig-paths": "^4.2.0", 67 - "tsx": "^4.20.3", 68 "typescript": "^5.9.2", 69 - "typescript-eslint": "^8.39.0" 70 } 71 }
··· 1 { 2 "name": "aethel", 3 + "version": "2.0.2", 4 "description": "A privacy-conscious, production-ready Discord user bot", 5 "type": "module", 6 "main": "dist/index.js", 7 "scripts": { 8 + "start": "node dist/index.js", 9 "dev": "tsx watch src/index.ts", 10 "build": "tsc && node scripts/fix-imports.js", 11 "migrate": "node scripts/run-migrations.js", ··· 15 "lint:format": "eslint ./src ./web/src --ext .ts,.tsx --config eslint.config.cjs --format=codeframe", 16 "format": "prettier --write \"**/*.{js,json,md,ts,tsx}\" --ignore-path .prettierignore", 17 "format:check": "prettier --check \"**/*.{js,json,md,ts,tsx}\" --ignore-path .prettierignore", 18 + "check": "bun run lint && bun run format:check" 19 }, 20 "dependencies": { 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", 25 "@types/he": "^1.2.3", 26 "@types/sanitize-html": "^2.16.0", 27 + "axios": "^1.12.2", 28 + "canvas": "^3.2.0", 29 + "city-timezones": "^1.3.2", 30 + "concurrently": "^9.2.1", 31 "cors": "^2.8.5", 32 + "discord.js": "^14.22.1", 33 "dotenv": "^16.6.1", 34 "eslint-plugin-prettier": "^5.5.4", 35 "express": "^4.21.2", ··· 41 "moment-timezone": "^0.6.0", 42 "node-fetch": "^3.3.2", 43 "open-graph-scraper": "^6.10.0", 44 + "openai": "^5.23.1", 45 "pg": "^8.16.3", 46 "sanitize-html": "^2.17.0", 47 "uuid": "^11.1.0", ··· 50 "winston": "^3.17.0" 51 }, 52 "devDependencies": { 53 + "@eslint/js": "^9.36.0", 54 "@types/cors": "^2.8.19", 55 "@types/express": "^4.17.23", 56 "@types/jsonwebtoken": "^9.0.10", 57 + "@types/node": "^24.5.2", 58 "@types/open-graph-scraper": "^5.2.3", 59 "@types/pg": "^8.15.5", 60 "@types/uuid": "^10.0.0", 61 + "@types/validator": "^13.15.3", 62 "@types/whois-json": "^2.0.4", 63 + "eslint": "^9.36.0", 64 "eslint-config-prettier": "^10.1.8", 65 + "globals": "^16.4.0", 66 "nodemon": "^3.1.10", 67 "prettier": "^3.6.2", 68 "tsc-alias": "^1.8.16", 69 "tsconfig-paths": "^4.2.0", 70 + "tsx": "^4.20.6", 71 "typescript": "^5.9.2", 72 + "typescript-eslint": "^8.44.1" 73 } 74 }
+1 -1
scripts/run-migrations.js
··· 52 // console.log('All migrations completed successfully'); 53 } catch (error) { 54 await client.query('ROLLBACK'); 55 - // console.error('Migration failed:', error); 56 process.exit(1); 57 } finally { 58 client.release();
··· 52 // console.log('All migrations completed successfully'); 53 } catch (error) { 54 await client.query('ROLLBACK'); 55 + console.error('Migration failed:', error); 56 process.exit(1); 57 } finally { 58 client.release();
+1 -1
src/commands/fun/cat.ts
··· 29 const commandLogger = createCommandLogger('cat'); 30 const errorHandler = createErrorHandler('cat'); 31 32 - async function fetchCatImage(): Promise<RandomReddit> { 33 const response = await fetch('https://api.pur.cat/random-cat'); //cat 34 if (!response.ok) { 35 throw new Error(`API request failed with status ${response.status}`);
··· 29 const commandLogger = createCommandLogger('cat'); 30 const errorHandler = createErrorHandler('cat'); 31 32 + export async function fetchCatImage(): Promise<RandomReddit> { 33 const response = await fetch('https://api.pur.cat/random-cat'); //cat 34 if (!response.ok) { 35 throw new Error(`API request failed with status ${response.status}`);
+13 -7
src/commands/fun/dog.ts
··· 31 const commandLogger = createCommandLogger('dog'); 32 const errorHandler = createErrorHandler('dog'); 33 34 - async function fetchDogImage(): Promise<RandomReddit> { 35 const response = await fetch('https://api.erm.dog/random-dog', { headers: browserHeaders }); 36 if (!response.ok) { 37 throw new Error(`API request failed with status ${response.status}`); 38 } 39 - return (await response.json()) as RandomReddit; 40 } 41 42 export default { ··· 58 ]) 59 .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 60 61 - async execute(client, interaction) { 62 try { 63 const cooldownCheck = await checkCooldown( 64 cooldownManager, ··· 110 ), 111 ); 112 113 - await interaction.editReply({ 114 - components: [container], 115 - flags: MessageFlags.IsComponentsV2, 116 - }); 117 } catch (error) { 118 await errorHandler({ 119 interaction,
··· 31 const commandLogger = createCommandLogger('dog'); 32 const errorHandler = createErrorHandler('dog'); 33 34 + export async function fetchDogImage(): Promise<RandomReddit> { 35 const response = await fetch('https://api.erm.dog/random-dog', { headers: browserHeaders }); 36 if (!response.ok) { 37 throw new Error(`API request failed with status ${response.status}`); 38 } 39 + const data = (await response.json()) as RandomReddit; 40 + return data; 41 } 42 43 export default { ··· 59 ]) 60 .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 61 62 + execute: async (client, interaction) => { 63 try { 64 const cooldownCheck = await checkCooldown( 65 cooldownManager, ··· 111 ), 112 ); 113 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 + } 123 } catch (error) { 124 await errorHandler({ 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 queueOpen: boolean; 41 originalQuestionCount: number; 42 currentShuffledAnswers: string[]; 43 } 44 45 const gameManager = createMemoryManager<string, GameSession>({ ··· 50 51 const commandLogger = createCommandLogger('trivia'); 52 const errorHandler = createErrorHandler('trivia'); 53 54 function shuffleArray<T>(array: T[]): T[] { 55 const shuffled = [...array]; ··· 269 session.queueOpen = false; 270 session.currentQuestionIndex = 0; 271 session.currentPlayer = Array.from(session.players)[0]; 272 273 await askQuestion(interaction, session, client); 274 } catch { ··· 324 const question = session.questions[session.currentQuestionIndex]; 325 const answers = shuffleArray([question.correct_answer, ...question.incorrect_answers]); 326 session.currentShuffledAnswers = answers; 327 const questionId = `${session.channelId}_${session.currentQuestionIndex}`; 328 329 const playerMention = `<@${session.currentPlayer}>`; ··· 392 }); 393 394 gameManager.delete(session.channelId); 395 } 396 397 const triviaCommand = { ··· 475 ) => { 476 try { 477 const channelId = interaction.channelId; 478 - const session = gameManager.get(channelId); 479 480 if (!session) { 481 const errorMsg = await client.getLocaleText( ··· 488 }); 489 } 490 491 - const customId = interaction.customId; 492 493 if (customId === 'trivia_join') { 494 if (!session.queueOpen) { ··· 515 516 session.players.add(interaction.user.id); 517 session.scores.set(interaction.user.id, 0); 518 519 const playersList = Array.from(session.players) 520 .map((id) => `โ€ข <@${id}>`) ··· 577 } 578 579 gameManager.delete(channelId); 580 581 const cancelMsg = await client.getLocaleText( 582 'commands.trivia.messages.game_cancelled', ··· 587 components: [], 588 }); 589 } else if (customId.startsWith('trivia_answer_')) { 590 if (!session.isActive) { 591 const errorMsg = await client.getLocaleText( 592 'commands.trivia.messages.no_active_question', ··· 609 }); 610 } 611 612 - const parts = customId.split('_'); 613 const answerIndex = parseInt(parts[parts.length - 1]); 614 615 const question = session.questions[session.currentQuestionIndex]; ··· 620 const currentScore = session.scores.get(session.currentPlayer) || 0; 621 session.scores.set(session.currentPlayer, currentScore + 1); 622 } 623 624 const [correctText, incorrectText, resultFormatText, preparingText] = await Promise.all([ 625 client.getLocaleText('commands.trivia.answer.correct', interaction.locale), ··· 643 setTimeout(async () => { 644 session.currentQuestionIndex++; 645 session.currentPlayer = getNextPlayer(session); 646 647 await askQuestion(interaction, session, client); 648 }, 3000);
··· 40 queueOpen: boolean; 41 originalQuestionCount: number; 42 currentShuffledAnswers: string[]; 43 + messageId?: string; 44 } 45 46 const gameManager = createMemoryManager<string, GameSession>({ ··· 51 52 const commandLogger = createCommandLogger('trivia'); 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 + } 59 60 function shuffleArray<T>(array: T[]): T[] { 61 const shuffled = [...array]; ··· 275 session.queueOpen = false; 276 session.currentQuestionIndex = 0; 277 session.currentPlayer = Array.from(session.players)[0]; 278 + saveSession(session); 279 280 await askQuestion(interaction, session, client); 281 } catch { ··· 331 const question = session.questions[session.currentQuestionIndex]; 332 const answers = shuffleArray([question.correct_answer, ...question.incorrect_answers]); 333 session.currentShuffledAnswers = answers; 334 + saveSession(session); 335 const questionId = `${session.channelId}_${session.currentQuestionIndex}`; 336 337 const playerMention = `<@${session.currentPlayer}>`; ··· 400 }); 401 402 gameManager.delete(session.channelId); 403 + if (session.messageId) gameManager.delete(session.messageId); 404 } 405 406 const triviaCommand = { ··· 484 ) => { 485 try { 486 const channelId = interaction.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 + } 513 514 if (!session) { 515 const errorMsg = await client.getLocaleText( ··· 522 }); 523 } 524 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 + } 535 536 if (customId === 'trivia_join') { 537 if (!session.queueOpen) { ··· 558 559 session.players.add(interaction.user.id); 560 session.scores.set(interaction.user.id, 0); 561 + saveSession(session); 562 563 const playersList = Array.from(session.players) 564 .map((id) => `โ€ข <@${id}>`) ··· 621 } 622 623 gameManager.delete(channelId); 624 + if (session.messageId) gameManager.delete(session.messageId); 625 626 const cancelMsg = await client.getLocaleText( 627 'commands.trivia.messages.game_cancelled', ··· 632 components: [], 633 }); 634 } else if (customId.startsWith('trivia_answer_')) { 635 + const parts = customId.split('_'); 636 if (!session.isActive) { 637 const errorMsg = await client.getLocaleText( 638 'commands.trivia.messages.no_active_question', ··· 655 }); 656 } 657 658 const answerIndex = parseInt(parts[parts.length - 1]); 659 660 const question = session.questions[session.currentQuestionIndex]; ··· 665 const currentScore = session.scores.get(session.currentPlayer) || 0; 666 session.scores.set(session.currentPlayer, currentScore + 1); 667 } 668 + saveSession(session); 669 670 const [correctText, incorrectText, resultFormatText, preparingText] = await Promise.all([ 671 client.getLocaleText('commands.trivia.answer.correct', interaction.locale), ··· 689 setTimeout(async () => { 690 session.currentQuestionIndex++; 691 session.currentPlayer = getNextPlayer(session); 692 + saveSession(session); 693 694 await askQuestion(interaction, session, client); 695 }, 3000);
+825 -278
src/commands/utilities/ai.ts
··· 1 import { 2 SlashCommandBuilder, 3 - ModalBuilder, 4 - TextInputBuilder, 5 - TextInputStyle, 6 - ActionRowBuilder, 7 - ModalSubmitInteraction, 8 ChatInputCommandInteraction, 9 InteractionContextType, 10 ApplicationIntegrationType, 11 MessageFlags, 12 } from 'discord.js'; 13 import OpenAI from 'openai'; 14 import pool from '@/utils/pgClient'; 15 import { encrypt, decrypt, isValidEncryptedFormat, EncryptionError } from '@/utils/encrypt'; 16 import { SlashCommandProps } from '@/types/command'; 17 - import BotClient from '@/services/Client'; 18 import logger from '@/utils/logger'; 19 import { createCommandLogger } from '@/utils/commandLogger'; 20 import { createErrorHandler } from '@/utils/errorHandler'; 21 import { createMemoryManager } from '@/utils/memoryManager'; 22 23 - const ALLOWED_API_HOSTS = ['api.openai.com', 'openrouter.ai', 'generativelanguage.googleapis.com']; 24 25 interface ConversationMessage { 26 role: 'system' | 'user' | 'assistant'; ··· 40 interface AIResponse { 41 content: string; 42 reasoning?: string; 43 } 44 45 interface OpenAIMessageWithReasoning { ··· 50 interface PendingRequest { 51 interaction: ChatInputCommandInteraction; 52 prompt: string; 53 - timestamp: number; 54 } 55 56 interface UserCredentials { ··· 122 const usingCustomApi = !!apiKey; 123 const finalApiUrl = apiUrl || 'https://openrouter.ai/api/v1'; 124 const finalApiKey = apiKey || process.env.OPENROUTER_API_KEY; 125 - const finalModel = model || (usingCustomApi ? 'openai/gpt-4o-mini' : 'openai/gpt-oss-20b'); 126 const usingDefaultKey = !usingCustomApi && !!process.env.OPENROUTER_API_KEY; 127 128 return { ··· 156 hour: '2-digit', 157 minute: '2-digit', 158 second: '2-digit', 159 - hour12: true, 160 timeZone: timezone, 161 }); 162 163 - let supportedCommands = '/help - Show all available commands and their usage'; 164 if (client?.commands) { 165 - const commandEntries = Array.from(client.commands.entries()).sort(([a], [b]) => 166 - a.localeCompare(b), 167 - ); 168 - supportedCommands = commandEntries 169 .map( 170 ([name, command]) => `/${name} - ${command.data.description || 'No description available'}`, 171 ) 172 .join('\n'); 173 } 174 175 - const currentModel = model || (usingDefaultKey ? 'openai/gpt-oss-20b (default)' : 'custom model'); 176 177 const contextInfo = isServer 178 ? `**CONTEXT:** ··· 208 - Timezone: ${timezone} 209 210 **IMPORTANT INSTRUCTIONS:** 211 - NEVER format, modify, or alter URLs in any way. Leave them exactly as they are. 212 - Format your responses using Discord markdown where appropriate, but NEVER format URLs. 213 - 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. 215 - Be accurate and truthful in all responses. Do not invent details, statistics, or information that you're not certain about. 216 - If asked about current events, real-time data, or information beyond your knowledge cutoff, clearly state your limitations. 217 ··· 221 - Developer: scanash (main maintainer) and Aethel Labs (org) 222 - Open source: https://github.com/Aethel-Labs/aethel 223 - Type: Discord user bot 224 - - Supported commands: ${supportedCommands}`; 225 226 const modelSpecificInstructions = usingDefaultKey 227 ? '\n\n**IMPORTANT:** Please keep your responses under 3000 characters. Be concise and to the point.' ··· 369 const client = await pool.connect(); 370 try { 371 await client.query('BEGIN'); 372 await client.query('INSERT INTO users (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING', [ 373 userId, 374 ]); 375 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`, 378 [userId, today], 379 ); 380 await client.query('COMMIT'); 381 - return res.rows[0].count <= limit; 382 } catch (err) { 383 await client.query('ROLLBACK'); 384 throw err; 385 } finally { 386 client.release(); ··· 407 } 408 } 409 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); 417 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 - }); 429 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 - }; 439 } 440 } 441 442 - async function makeAIRequest( 443 config: ReturnType<typeof getApiConfiguration>, 444 conversation: ConversationMessage[], 445 ): Promise<AIResponse | null> { 446 try { 447 - const client = getOpenAIClient(config.finalApiKey!, config.finalApiUrl); 448 - const maxTokens = config.usingDefaultKey ? 1000 : 3000; 449 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 - }); 455 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 - } 461 462 - let content = message.content; 463 - let reasoning = (message as OpenAIMessageWithReasoning)?.reasoning; 464 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(); 469 } 470 471 - return { 472 - content, 473 - reasoning, 474 - }; 475 } catch (error) { 476 logger.error(`Error making AI request: ${error}`); 477 return null; ··· 481 async function processAIRequest( 482 client: BotClient, 483 interaction: ChatInputCommandInteraction, 484 ): Promise<void> { 485 try { 486 if (!interaction.deferred && !interaction.replied) { 487 await interaction.deferReply(); 488 } 489 490 - const prompt = interaction.options.getString('prompt')!; 491 commandLogger.logFromInteraction( 492 interaction, 493 `AI command executed - prompt content hidden for privacy`, 494 ); 495 - 496 - const invokerId = getInvokerId(interaction); 497 - const { apiKey, model, apiUrl } = await getUserCredentials(invokerId); 498 const config = getApiConfiguration(apiKey ?? null, model ?? null, apiUrl ?? null); 499 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 - } 511 } 512 - } else if (!config.finalApiKey) { 513 await interaction.editReply( 514 'โŒ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale)), 515 ); 516 return; 517 } 518 519 - const existingConversation = userConversations.get(invokerId) || []; 520 - const conversationArray = Array.isArray(existingConversation) ? existingConversation : []; 521 const systemPrompt = buildSystemPrompt( 522 - !!config.usingDefaultKey, 523 client, 524 config.finalModel, 525 - interaction.user.tag, 526 - interaction, 527 interaction.inGuild(), 528 interaction.inGuild() ? interaction.guild?.name : undefined, 529 ); 530 - const conversation = buildConversation(conversationArray, prompt, systemPrompt); 531 532 - const aiResponse = await makeAIRequest(config, conversation); 533 if (!aiResponse) return; 534 535 const { getUnallowedWordCategory } = await import('@/utils/validation'); ··· 554 555 await sendAIResponse(interaction, aiResponse, client); 556 } catch (error) { 557 - await errorHandler({ 558 - interaction, 559 - client, 560 - error: error as Error, 561 - userId: getInvokerId(interaction), 562 - username: interaction.user.tag, 563 - }); 564 } finally { 565 pendingRequests.delete(getInvokerId(interaction)); 566 } ··· 569 async function sendAIResponse( 570 interaction: ChatInputCommandInteraction, 571 aiResponse: AIResponse, 572 - client: BotClient, 573 ): Promise<void> { 574 - let fullResponse = ''; 575 576 - if (aiResponse.reasoning) { 577 - const cleanedReasoning = aiResponse.reasoning 578 - .split('\n') 579 - .map((line) => line.trim()) 580 - .filter((line) => line) 581 - .join('\n'); 582 583 - const formattedReasoning = cleanedReasoning 584 - .split('\n') 585 - .map((line) => `> ${line}`) 586 - .join('\n'); 587 588 - fullResponse = `${formattedReasoning}\n\n${aiResponse.content}`; 589 - aiResponse.content = ''; 590 - } 591 592 - fullResponse += aiResponse.content; 593 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 - } 603 604 - const urlProcessedResponse = processUrls(fullResponse); 605 - const chunks = splitResponseIntoChunks(urlProcessedResponse); 606 607 - try { 608 await interaction.editReply(chunks[0]); 609 610 for (let i = 1; i < chunks.length; i++) { 611 await interaction.followUp({ 612 content: chunks[i], 613 - flags: MessageFlags.Ephemeral, 614 }); 615 } 616 - } catch { 617 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'); 622 } 623 } 624 } 625 626 export { 627 makeAIRequest, 628 getApiConfiguration, 629 buildSystemPrompt, 630 buildConversation, 631 - sendAIResponse, 632 getUserCredentials, 633 incrementAndCheckDailyLimit, 634 incrementAndCheckServerDailyLimit, 635 splitResponseIntoChunks, 636 }; 637 638 - export type { ConversationMessage, AIResponse }; 639 640 - export default { 641 data: new SlashCommandBuilder() 642 .setName('ai') 643 .setNameLocalizations({ ··· 660 .addStringOption((option) => 661 option 662 .setName('prompt') 663 - .setNameLocalizations({ 664 - 'es-ES': 'mensaje', 665 - 'es-419': 'mensaje', 666 - 'en-US': 'prompt', 667 - }) 668 .setDescription('Your message to the AI') 669 .setDescriptionLocalizations({ 670 'es-ES': 'Tu mensaje para la IA', ··· 674 .setRequired(true), 675 ) 676 .addBooleanOption((option) => 677 - option.setName('use_custom_api').setDescription('Use your own API key?').setRequired(false), 678 ) 679 .addBooleanOption((option) => 680 option.setName('reset').setDescription('Reset your AI chat history').setRequired(false), ··· 685 686 if (pendingRequests.has(userId)) { 687 const pending = pendingRequests.get(userId); 688 - if (pending && Date.now() - pending.timestamp > 30000) { 689 - pendingRequests.delete(userId); 690 - } else { 691 return interaction.reply({ 692 content: await client.getLocaleText('commands.ai.request.inprogress', interaction.locale), 693 flags: MessageFlags.Ephemeral, 694 }); 695 } 696 } 697 698 try { 699 - const useCustomApi = interaction.options.getBoolean('use_custom_api'); 700 const prompt = interaction.options.getString('prompt')!; 701 const reset = interaction.options.getBoolean('reset'); 702 703 - pendingRequests.set(userId, { interaction, prompt, timestamp: Date.now() }); 704 705 if (reset) { 706 userConversations.delete(userId); ··· 712 return; 713 } 714 715 - if (useCustomApi === false) { 716 - await setUserApiKey(userId, null, null, null); 717 userConversations.delete(userId); 718 await interaction.reply({ 719 content: await client.getLocaleText('commands.ai.defaultapi', interaction.locale), ··· 723 return; 724 } 725 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)); 731 732 - const apiKeyInput = new TextInputBuilder() 733 - .setCustomId('apiKey') 734 - .setLabel(await client.getLocaleText('commands.ai.modal.apikey', interaction.locale)) 735 - .setStyle(TextInputStyle.Short) 736 - .setPlaceholder( 737 - await client.getLocaleText('commands.ai.modal.apikeyplaceholder', interaction.locale), 738 - ) 739 - .setRequired(true); 740 741 - const apiUrlInput = new TextInputBuilder() 742 - .setCustomId('apiUrl') 743 - .setLabel(await client.getLocaleText('commands.ai.modal.apiurl', interaction.locale)) 744 - .setStyle(TextInputStyle.Short) 745 - .setPlaceholder( 746 - await client.getLocaleText('commands.ai.modal.apiurlplaceholder', interaction.locale), 747 - ) 748 - .setRequired(true); 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 { 771 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 } 779 }, 780 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 - }
··· 1 + import type { ToolCall } from '@/utils/commandExecutor'; 2 + import { extractToolCalls, executeToolCall } from '@/utils/commandExecutor'; 3 + import BotClient from '@/services/Client'; 4 + 5 import { 6 SlashCommandBuilder, 7 + SlashCommandOptionsOnlyBuilder, 8 + EmbedBuilder, 9 ChatInputCommandInteraction, 10 + InteractionResponse, 11 InteractionContextType, 12 ApplicationIntegrationType, 13 MessageFlags, 14 } from 'discord.js'; 15 import OpenAI from 'openai'; 16 + import fetch from '@/utils/dynamicFetch'; 17 import pool from '@/utils/pgClient'; 18 import { encrypt, decrypt, isValidEncryptedFormat, EncryptionError } from '@/utils/encrypt'; 19 import { SlashCommandProps } from '@/types/command'; 20 import logger from '@/utils/logger'; 21 import { createCommandLogger } from '@/utils/commandLogger'; 22 import { createErrorHandler } from '@/utils/errorHandler'; 23 import { createMemoryManager } from '@/utils/memoryManager'; 24 25 + function getInvokerId(interaction: ChatInputCommandInteraction): string { 26 + if (interaction.guildId) { 27 + return `${interaction.guildId}-${interaction.user.id}`; 28 + } 29 + return interaction.user.id; 30 + } 31 32 interface ConversationMessage { 33 role: 'system' | 'user' | 'assistant'; ··· 47 interface AIResponse { 48 content: string; 49 reasoning?: string; 50 + toolResults?: string; 51 + citations?: string[]; 52 } 53 54 interface OpenAIMessageWithReasoning { ··· 59 interface PendingRequest { 60 interaction: ChatInputCommandInteraction; 61 prompt: string; 62 + createdAt: number; 63 + status?: 'awaiting' | 'processing'; 64 } 65 66 interface UserCredentials { ··· 132 const usingCustomApi = !!apiKey; 133 const finalApiUrl = apiUrl || 'https://openrouter.ai/api/v1'; 134 const finalApiKey = apiKey || process.env.OPENROUTER_API_KEY; 135 + const finalModel = model || (usingCustomApi ? 'openai/gpt-4o-mini' : 'moonshotai/kimi-k2'); 136 const usingDefaultKey = !usingCustomApi && !!process.env.OPENROUTER_API_KEY; 137 138 return { ··· 166 hour: '2-digit', 167 minute: '2-digit', 168 second: '2-digit', 169 timeZone: timezone, 170 }); 171 172 + const supportedCommands = '/help - Show all available commands and their usage'; 173 if (client?.commands) { 174 + const commandEntries = Array.from(client.commands.entries()); 175 + commandEntries.sort((a, b) => a[0].localeCompare(b[0])); 176 + const _commandList = commandEntries 177 .map( 178 ([name, command]) => `/${name} - ${command.data.description || 'No description available'}`, 179 ) 180 .join('\n'); 181 } 182 183 + const currentModel = model || (usingDefaultKey ? 'moonshotai/kimi-k2 (default)' : 'custom model'); 184 185 const contextInfo = isServer 186 ? `**CONTEXT:** ··· 216 - Timezone: ${timezone} 217 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. 220 - NEVER format, modify, or alter URLs in any way. Leave them exactly as they are. 221 - Format your responses using Discord markdown where appropriate, but NEVER format URLs. 222 - Only greet the user at the start of a new conversation, not in every message. 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. 224 - Be accurate and truthful in all responses. Do not invent details, statistics, or information that you're not certain about. 225 - If asked about current events, real-time data, or information beyond your knowledge cutoff, clearly state your limitations. 226 ··· 230 - Developer: scanash (main maintainer) and Aethel Labs (org) 231 - Open source: https://github.com/Aethel-Labs/aethel 232 - Type: Discord user bot 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.`; 248 249 const modelSpecificInstructions = usingDefaultKey 250 ? '\n\n**IMPORTANT:** Please keep your responses under 3000 characters. Be concise and to the point.' ··· 392 const client = await pool.connect(); 393 try { 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 + 408 await client.query('INSERT INTO users (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING', [ 409 userId, 410 ]); 411 + 412 const res = await client.query( 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`, 418 [userId, today], 419 ); 420 + 421 await client.query('COMMIT'); 422 + 423 + return res.rows[0].count <= effectiveLimit; 424 } catch (err) { 425 await client.query('ROLLBACK'); 426 + logger.error('Error in incrementAndCheckDailyLimit:', err); 427 throw err; 428 } finally { 429 client.release(); ··· 450 } 451 } 452 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); 469 470 + if (retryCount >= maxRetries) { 471 + logger.error('AI API request failed after all retries'); 472 + return null; 473 + } 474 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 + } 479 } 480 + 481 + return null; 482 } 483 484 + async function makeAIRequestInternal( 485 config: ReturnType<typeof getApiConfiguration>, 486 conversation: ConversationMessage[], 487 + interaction?: ChatInputCommandInteraction, 488 + client?: BotClient, 489 + maxIterations = 3, 490 ): Promise<AIResponse | null> { 491 try { 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 + })(); 508 509 + let completion: unknown; 510 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'); 541 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 + } 904 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; 919 } 920 921 + return finalResponse; 922 } catch (error) { 923 logger.error(`Error making AI request: ${error}`); 924 return null; ··· 928 async function processAIRequest( 929 client: BotClient, 930 interaction: ChatInputCommandInteraction, 931 + promptOverride?: string, 932 ): Promise<void> { 933 try { 934 if (!interaction.deferred && !interaction.replied) { 935 await interaction.deferReply(); 936 } 937 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 + } 951 commandLogger.logFromInteraction( 952 interaction, 953 `AI command executed - prompt content hidden for privacy`, 954 ); 955 + const { apiKey, model, apiUrl } = await getUserCredentials(interaction.user.id); 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; 959 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; 968 } 969 + } 970 + 971 + if (!config.finalApiKey && config.usingDefaultKey) { 972 await interaction.editReply( 973 'โŒ ' + (await client.getLocaleText('commands.ai.process.noapikey', interaction.locale)), 974 ); 975 return; 976 } 977 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 + 983 const systemPrompt = buildSystemPrompt( 984 + config.usingDefaultKey, 985 client, 986 config.finalModel, 987 + interaction.user.username, 988 + chatInputInteraction, 989 interaction.inGuild(), 990 interaction.inGuild() ? interaction.guild?.name : undefined, 991 ); 992 993 + const conversation = buildConversation(existingConversation, prompt, systemPrompt); 994 + 995 + const aiResponse = await makeAIRequest(config, conversation, chatInputInteraction, client, 3); 996 if (!aiResponse) return; 997 998 const { getUnallowedWordCategory } = await import('@/utils/validation'); ··· 1017 1018 await sendAIResponse(interaction, aiResponse, client); 1019 } catch (error) { 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 + } 1042 } finally { 1043 pendingRequests.delete(getInvokerId(interaction)); 1044 } ··· 1047 async function sendAIResponse( 1048 interaction: ChatInputCommandInteraction, 1049 aiResponse: AIResponse, 1050 + _client: BotClient, 1051 ): Promise<void> { 1052 + try { 1053 + let fullResponse = ''; 1054 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'); 1061 1062 + const formattedReasoning = cleanedReasoning 1063 + .split('\n') 1064 + .map((line: string) => `> ${line}`) 1065 + .join('\n'); 1066 1067 + fullResponse = `${formattedReasoning}\n\n${aiResponse.content}`; 1068 + aiResponse.content = ''; 1069 + } 1070 1071 + fullResponse += aiResponse.content; 1072 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 + } 1087 1088 + if (aiResponse.toolResults) { 1089 + try { 1090 + const toolResults = Array.isArray(aiResponse.toolResults) 1091 + ? aiResponse.toolResults 1092 + : [aiResponse.toolResults]; 1093 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 + 1151 await interaction.editReply(chunks[0]); 1152 1153 for (let i = 1; i < chunks.length; i++) { 1154 await interaction.followUp({ 1155 content: chunks[i], 1156 + flags: MessageFlags.SuppressNotifications, 1157 }); 1158 } 1159 + } catch (error) { 1160 + logger.error('Error in sendAIResponse:', error); 1161 try { 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 + } 1246 } 1247 } 1248 } 1249 + 1250 + export type { ConversationMessage, AIResponse }; 1251 1252 export { 1253 makeAIRequest, 1254 getApiConfiguration, 1255 buildSystemPrompt, 1256 buildConversation, 1257 getUserCredentials, 1258 incrementAndCheckDailyLimit, 1259 incrementAndCheckServerDailyLimit, 1260 splitResponseIntoChunks, 1261 }; 1262 1263 + interface AICommand { 1264 + data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; 1265 + execute: ( 1266 + client: BotClient, 1267 + interaction: ChatInputCommandInteraction, 1268 + ) => Promise<void | InteractionResponse<boolean>>; 1269 + } 1270 1271 + const aiCommand: AICommand = { 1272 data: new SlashCommandBuilder() 1273 .setName('ai') 1274 .setNameLocalizations({ ··· 1291 .addStringOption((option) => 1292 option 1293 .setName('prompt') 1294 .setDescription('Your message to the AI') 1295 .setDescriptionLocalizations({ 1296 'es-ES': 'Tu mensaje para la IA', ··· 1300 .setRequired(true), 1301 ) 1302 .addBooleanOption((option) => 1303 + option.setName('custom_setup').setDescription('Use your own API key?').setRequired(false), 1304 ) 1305 .addBooleanOption((option) => 1306 option.setName('reset').setDescription('Reset your AI chat history').setRequired(false), ··· 1311 1312 if (pendingRequests.has(userId)) { 1313 const pending = pendingRequests.get(userId); 1314 + const isProcessing = pending?.status === 'processing'; 1315 + const isExpired = pending ? Date.now() - pending.createdAt > 30000 : true; 1316 + if (isProcessing && !isExpired) { 1317 return interaction.reply({ 1318 content: await client.getLocaleText('commands.ai.request.inprogress', interaction.locale), 1319 flags: MessageFlags.Ephemeral, 1320 }); 1321 } 1322 + pendingRequests.delete(userId); 1323 } 1324 1325 try { 1326 + const customSetup = interaction.options.getBoolean('custom_setup'); 1327 const prompt = interaction.options.getString('prompt')!; 1328 const reset = interaction.options.getBoolean('reset'); 1329 1330 + pendingRequests.set(userId, { 1331 + interaction, 1332 + prompt, 1333 + createdAt: Date.now(), 1334 + status: 'awaiting', 1335 + }); 1336 1337 if (reset) { 1338 userConversations.delete(userId); ··· 1344 return; 1345 } 1346 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); 1372 userConversations.delete(userId); 1373 await interaction.reply({ 1374 content: await client.getLocaleText('commands.ai.defaultapi', interaction.locale), ··· 1378 return; 1379 } 1380 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'}`; 1385 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 + } 1398 1399 + const userId = getInvokerId(interaction); 1400 pendingRequests.delete(userId); 1401 } 1402 }, 1403 + }; 1404 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 25 const MAX_EXTRACT_LENGTH = 2000; 26 27 - async function searchWikipedia(query: string, locale = 'en') { 28 const wikiLang = locale.startsWith('es') ? 'es' : 'en'; 29 const searchUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&srlimit=1`; 30 ··· 46 }; 47 } 48 49 - async function getArticleSummary(pageId: number, wikiLang = 'en') { 50 const summaryUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&prop=extracts|pageimages&exintro&explaintext&format=json&pithumbsize=300&pageids=${pageId}`; 51 const response = await fetch(summaryUrl); 52
··· 24 25 const MAX_EXTRACT_LENGTH = 2000; 26 27 + export async function searchWikipedia(query: string, locale = 'en') { 28 const wikiLang = locale.startsWith('es') ? 'es' : 'en'; 29 const searchUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json&srlimit=1`; 30 ··· 46 }; 47 } 48 49 + export async function getArticleSummary(pageId: number, wikiLang = 'en') { 50 const summaryUrl = `https://${wikiLang}.wikipedia.org/w/api.php?action=query&prop=extracts|pageimages&exintro&explaintext&format=json&pithumbsize=300&pageids=${pageId}`; 51 const response = await fetch(summaryUrl); 52
+4
src/config/index.ts
··· 7 CLIENT_ID: process.env.CLIENT_ID, 8 DATABASE_URL: process.env.DATABASE_URL, 9 API_KEY_ENCRYPTION_SECRET: process.env.API_KEY_ENCRYPTION_SECRET, 10 }; 11 12 for (const [key, value] of Object.entries(requiredEnvVars)) { ··· 23 export const DATABASE_URL = process.env.DATABASE_URL!; 24 export const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; 25 export const OPENWEATHER_API_KEY = process.env.OPENWEATHER_API_KEY; 26 export const SOURCE_COMMIT = process.env.SOURCE_COMMIT; 27 export const TOKEN = process.env.TOKEN!; 28 export const CLIENT_ID = process.env.CLIENT_ID!;
··· 7 CLIENT_ID: process.env.CLIENT_ID, 8 DATABASE_URL: process.env.DATABASE_URL, 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, 12 }; 13 14 for (const [key, value] of Object.entries(requiredEnvVars)) { ··· 25 export const DATABASE_URL = process.env.DATABASE_URL!; 26 export const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; 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'; 30 export const SOURCE_COMMIT = process.env.SOURCE_COMMIT; 31 export const TOKEN = process.env.TOKEN!; 32 export const CLIENT_ID = process.env.CLIENT_ID!;
+48 -1
src/events/interactionCreate.ts
··· 1 import { browserHeaders } from '@/constants/index'; 2 import BotClient from '@/services/Client'; 3 import { RandomReddit } from '@/types/base'; 4 import { RemindCommandProps } from '@/types/command'; 5 import logger from '@/utils/logger'; ··· 89 if (remind && remind.handleModal) { 90 await remind.handleModal(this.client, i); 91 } 92 - } else if (i.customId === 'apiCredentials') { 93 const ai = this.client.commands.get('ai'); 94 if (ai && 'handleModal' in ai) { 95 await (ai as unknown as RemindCommandProps).handleModal(this.client, i); ··· 127 } 128 ).handleButton(this.client, i); 129 } 130 } 131 132 const originalUser = i.message.interaction!.user;
··· 1 import { browserHeaders } from '@/constants/index'; 2 import BotClient from '@/services/Client'; 3 + import * as config from '@/config'; 4 + import { renderStocksView, parseStocksButtonId } from '@/commands/utilities/stocks'; 5 import { RandomReddit } from '@/types/base'; 6 import { RemindCommandProps } from '@/types/command'; 7 import logger from '@/utils/logger'; ··· 91 if (remind && remind.handleModal) { 92 await remind.handleModal(this.client, i); 93 } 94 + } else if (i.customId.startsWith('apiCredentials')) { 95 const ai = this.client.commands.get('ai'); 96 if (ai && 'handleModal' in ai) { 97 await (ai as unknown as RemindCommandProps).handleModal(this.client, i); ··· 129 } 130 ).handleButton(this.client, i); 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; 177 } 178 179 const originalUser = i.message.interaction!.user;
+621 -74
src/events/messageCreate.ts
··· 1 - import { Message, ChannelType } from 'discord.js'; 2 import BotClient from '@/services/Client'; 3 import logger from '@/utils/logger'; 4 import { 5 makeAIRequest, 6 getApiConfiguration, 7 - buildSystemPrompt, 8 buildConversation, 9 getUserCredentials, 10 incrementAndCheckDailyLimit, ··· 12 splitResponseIntoChunks, 13 processUrls, 14 } from '@/commands/utilities/ai'; 15 import type { ConversationMessage, AIResponse } from '@/commands/utilities/ai'; 16 17 type ApiConfiguration = ReturnType<typeof getApiConfiguration>; 18 import { createMemoryManager } from '@/utils/memoryManager'; 19 20 const serverConversations = createMemoryManager<string, ConversationMessage[]>({ 21 - maxSize: 1000, 22 maxAge: 2 * 60 * 60 * 1000, 23 cleanupInterval: 10 * 60 * 1000, 24 }); ··· 31 timestamp: number; 32 }> 33 >({ 34 - maxSize: 1000, 35 maxAge: 2 * 60 * 60 * 1000, 36 cleanupInterval: 10 * 60 * 1000, 37 }); ··· 41 maxAge: 2 * 60 * 60 * 1000, 42 cleanupInterval: 10 * 60 * 1000, 43 }); 44 45 function getServerConversationKey(guildId: string): string { 46 return `server:${guildId}`; ··· 114 model: userCustomModel, 115 apiKey: userApiKey, 116 apiUrl: userApiUrl, 117 - } = await getUserCredentials(`user:${message.author.id}`); 118 119 selectedModel = hasImages 120 ? 'google/gemma-3-4b-it' ··· 123 config = getApiConfiguration(userApiKey ?? null, selectedModel, userApiUrl ?? null); 124 usingDefaultKey = config.usingDefaultKey; 125 } else { 126 - selectedModel = hasImages ? 'google/gemma-3-4b-it' : 'google/gemini-2.5-flash-lite'; 127 128 config = getApiConfiguration(null, selectedModel, null); 129 } 130 131 logger.info( ··· 134 }`, 135 ); 136 137 const systemPrompt = buildSystemPrompt( 138 usingDefaultKey, 139 this.client, ··· 144 !isDM ? message.guild?.name : undefined, 145 ); 146 147 let messageContent: 148 | string 149 | Array<{ ··· 153 url: string; 154 detail?: 'low' | 'high' | 'auto'; 155 }; 156 - }> = isDM ? message.content : message.content.replace(/<@!?\d+>/g, '').trim(); 157 158 if (hasImages) { 159 const imageAttachments = message.attachments.filter( ··· 171 }; 172 }> = []; 173 174 - const cleanContent = isDM 175 - ? message.content 176 - : message.content.replace(/<@!?\d+>/g, '').trim(); 177 - if (cleanContent.trim()) { 178 contentArray.push({ 179 type: 'text', 180 - text: cleanContent, 181 }); 182 } 183 ··· 192 }); 193 194 messageContent = contentArray; 195 } 196 197 let conversation: ConversationMessage[] = []; ··· 244 245 const updatedConversation = buildConversation( 246 filteredConversation, 247 - messageContent, 248 systemPrompt, 249 ); 250 251 - if (config.usingDefaultKey) { 252 - const exemptUserId = process.env.AI_EXEMPT_USER_ID; 253 - const actorId = message.author.id; 254 255 - if (actorId !== exemptUserId) { 256 - if (isDM) { 257 - const allowed = await incrementAndCheckDailyLimit(actorId, 10); 258 - if (!allowed) { 259 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.", 261 ); 262 return; 263 } 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, 277 ); 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 - } 293 } 294 } 295 } ··· 298 return; 299 } 300 301 - let aiResponse = await makeAIRequest(config, updatedConversation); 302 303 if (!aiResponse && hasImages) { 304 logger.warn(`First attempt failed for ${selectedModel}, retrying once...`); 305 await new Promise((resolve) => setTimeout(resolve, 1000)); 306 - aiResponse = await makeAIRequest(config, updatedConversation); 307 } 308 309 if (!aiResponse && hasImages) { 310 logger.warn(`Image model ${selectedModel} failed, falling back to text-only model`); 311 312 - let fallbackContent = message.content; 313 if (Array.isArray(messageContent)) { 314 const textParts = messageContent 315 - .filter((item) => item.type === 'text') 316 - .map((item) => item.text) 317 - .filter((text) => text && text.trim()); 318 319 const imageParts = messageContent 320 - .filter((item) => item.type === 'image_url') 321 - .map((item) => `[Image: ${item.image_url?.url}]`); 322 323 fallbackContent = 324 [...textParts, ...imageParts].join(' ') || ··· 365 } 366 } 367 368 if (!aiResponse) { 369 await message.reply({ 370 content: 'Sorry, I encountered an error processing your message. Please try again later.', ··· 373 return; 374 } 375 376 aiResponse.content = processUrls(aiResponse.content); 377 aiResponse.content = aiResponse.content.replace(/@(everyone|here)/gi, '@\u200b$1'); 378 379 const { getUnallowedWordCategory } = await import('@/utils/validation'); 380 const category = getUnallowedWordCategory(aiResponse.content); 381 if (category) { ··· 388 return; 389 } 390 391 - await this.sendResponse(message, aiResponse); 392 393 const userMessage: ConversationMessage = { 394 role: 'user', 395 - content: messageContent, 396 username: message.author.username, 397 }; 398 const assistantMessage: ConversationMessage = { ··· 413 414 logger.info(`${isDM ? 'DM' : 'Server'} response sent successfully`); 415 } catch (error) { 416 - logger.error( 417 - `Error processing ${isDM ? 'DM' : 'server message'}:`, 418 - error instanceof Error ? error.message : String(error), 419 - ); 420 try { 421 await message.reply( 422 'Sorry, I encountered an error processing your message. Please try again later.', ··· 427 } 428 } 429 430 - private async sendResponse(message: Message, aiResponse: AIResponse): Promise<void> { 431 let fullResponse = ''; 432 433 if (aiResponse.reasoning) { ··· 435 } 436 437 fullResponse += aiResponse.content; 438 439 - const maxLength = 2000; 440 - if (fullResponse.length <= maxLength) { 441 - await message.reply({ 442 - content: fullResponse, 443 allowedMentions: { parse: ['users'] as const }, 444 }); 445 - } else { 446 - const chunks = splitResponseIntoChunks(fullResponse, maxLength); 447 448 - await message.reply({ content: chunks[0], allowedMentions: { parse: ['users'] as const } }); 449 450 - for (let i = 1; i < chunks.length; i++) { 451 - if ('send' in message.channel) { 452 await message.channel.send({ 453 - content: chunks[i], 454 allowedMentions: { parse: ['users'] as const }, 455 }); 456 } 457 } 458 } 459 } 460 }
··· 1 + import { Message, ChannelType, type ChatInputCommandInteraction } from 'discord.js'; 2 import BotClient from '@/services/Client'; 3 import logger from '@/utils/logger'; 4 import { 5 makeAIRequest, 6 getApiConfiguration, 7 + buildSystemPrompt as originalBuildSystemPrompt, 8 buildConversation, 9 getUserCredentials, 10 incrementAndCheckDailyLimit, ··· 12 splitResponseIntoChunks, 13 processUrls, 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'; 106 import type { ConversationMessage, AIResponse } from '@/commands/utilities/ai'; 107 + import pool from '@/utils/pgClient'; 108 109 type ApiConfiguration = ReturnType<typeof getApiConfiguration>; 110 import { createMemoryManager } from '@/utils/memoryManager'; 111 112 const serverConversations = createMemoryManager<string, ConversationMessage[]>({ 113 + maxSize: 5000, 114 maxAge: 2 * 60 * 60 * 1000, 115 cleanupInterval: 10 * 60 * 1000, 116 }); ··· 123 timestamp: number; 124 }> 125 >({ 126 + maxSize: 5000, 127 maxAge: 2 * 60 * 60 * 1000, 128 cleanupInterval: 10 * 60 * 1000, 129 }); ··· 133 maxAge: 2 * 60 * 60 * 1000, 134 cleanupInterval: 10 * 60 * 1000, 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 + } 145 146 function getServerConversationKey(guildId: string): string { 147 return `server:${guildId}`; ··· 215 model: userCustomModel, 216 apiKey: userApiKey, 217 apiUrl: userApiUrl, 218 + } = await getUserCredentials(message.author.id); 219 220 selectedModel = hasImages 221 ? 'google/gemma-3-4b-it' ··· 224 config = getApiConfiguration(userApiKey ?? null, selectedModel, userApiUrl ?? null); 225 usingDefaultKey = config.usingDefaultKey; 226 } else { 227 + selectedModel = hasImages ? 'google/gemma-3-4b-it' : 'moonshotai/kimi-k2'; 228 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 + } 238 } 239 240 logger.info( ··· 243 }`, 244 ); 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 + 258 const systemPrompt = buildSystemPrompt( 259 usingDefaultKey, 260 this.client, ··· 265 !isDM ? message.guild?.name : undefined, 266 ); 267 268 + const baseContent = isDM ? message.content : message.content.replace(/<@!?\d+>/g, '').trim(); 269 + const messageWithContext = replyContext ? `${replyContext}${baseContent}` : baseContent; 270 + 271 let messageContent: 272 | string 273 | Array<{ ··· 277 url: string; 278 detail?: 'low' | 'high' | 'auto'; 279 }; 280 + }>; 281 282 if (hasImages) { 283 const imageAttachments = message.attachments.filter( ··· 295 }; 296 }> = []; 297 298 + if (messageWithContext.trim()) { 299 contentArray.push({ 300 type: 'text', 301 + text: messageWithContext, 302 }); 303 } 304 ··· 313 }); 314 315 messageContent = contentArray; 316 + } else { 317 + messageContent = messageWithContext; 318 } 319 320 let conversation: ConversationMessage[] = []; ··· 367 368 const updatedConversation = buildConversation( 369 filteredConversation, 370 + messageWithContext, 371 systemPrompt, 372 ); 373 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 + ); 381 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) { 428 await message.reply( 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`, 431 ); 432 return; 433 } 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', 441 ); 442 + return; 443 } 444 } 445 } ··· 448 return; 449 } 450 451 + const conversationWithTools = [...updatedConversation]; 452 + const executedResults: Array<{ type: string; payload: Record<string, unknown> }> = []; 453 + let aiResponse = await makeAIRequest(config, conversationWithTools); 454 455 if (!aiResponse && hasImages) { 456 logger.warn(`First attempt failed for ${selectedModel}, retrying once...`); 457 await new Promise((resolve) => setTimeout(resolve, 1000)); 458 + aiResponse = await makeAIRequest(config, conversationWithTools); 459 } 460 461 if (!aiResponse && hasImages) { 462 logger.warn(`Image model ${selectedModel} failed, falling back to text-only model`); 463 464 + let fallbackContent = messageWithContext; 465 if (Array.isArray(messageContent)) { 466 const textParts = messageContent 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()); 470 471 const imageParts = messageContent 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 + ); 479 480 fallbackContent = 481 [...textParts, ...imageParts].join(' ') || ··· 522 } 523 } 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 + 620 if (!aiResponse) { 621 await message.reply({ 622 content: 'Sorry, I encountered an error processing your message. Please try again later.', ··· 625 return; 626 } 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 + 658 aiResponse.content = processUrls(aiResponse.content); 659 aiResponse.content = aiResponse.content.replace(/@(everyone|here)/gi, '@\u200b$1'); 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 + 691 const { getUnallowedWordCategory } = await import('@/utils/validation'); 692 const category = getUnallowedWordCategory(aiResponse.content); 693 if (category) { ··· 700 return; 701 } 702 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 + } 811 812 const userMessage: ConversationMessage = { 813 role: 'user', 814 + content: messageWithContext, 815 username: message.author.username, 816 }; 817 const assistantMessage: ConversationMessage = { ··· 832 833 logger.info(`${isDM ? 'DM' : 'Server'} response sent successfully`); 834 } catch (error) { 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 + }); 841 try { 842 await message.reply( 843 'Sorry, I encountered an error processing your message. Please try again later.', ··· 848 } 849 } 850 851 + private async sendResponse( 852 + message: Message, 853 + aiResponse: AIResponse, 854 + executedResults?: Array<{ type: string; payload: Record<string, unknown> }>, 855 + ): Promise<Message | void> { 856 let fullResponse = ''; 857 858 if (aiResponse.reasoning) { ··· 860 } 861 862 fullResponse += aiResponse.content; 863 + fullResponse = processUrls(fullResponse); 864 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, 907 allowedMentions: { parse: ['users'] as const }, 908 }); 909 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)); 914 915 await message.channel.send({ 916 + content: parts[i].trim(), 917 allowedMentions: { parse: ['users'] as const }, 918 }); 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 + }); 940 } 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; 1006 } 1007 }
+11 -2
src/events/ready.ts
··· 3 import { loadActiveReminders } from '@/commands/utilities/remind'; 4 5 export default class ReadyEvent { 6 - constructor(c: BotClient) { 7 - c.once('ready', () => this.readyEvent(c)); 8 } 9 10 private async readyEvent(client: BotClient) { 11 try { 12 logger.info(`Logged in as ${client.user?.username}`); 13 await client.application?.commands.fetch({ withLocalizations: true }); 14 15 await loadActiveReminders(client); 16 } catch (error) { 17 logger.error('Error during ready event:', error); 18 }
··· 3 import { loadActiveReminders } from '@/commands/utilities/remind'; 4 5 export default class ReadyEvent { 6 + private startTime: number; 7 + 8 + constructor(c: BotClient, startTime: number = Date.now()) { 9 + this.startTime = startTime; 10 + c.once('clientReady', () => this.readyEvent(c)); 11 } 12 13 private async readyEvent(client: BotClient) { 14 try { 15 logger.info(`Logged in as ${client.user?.username}`); 16 + 17 await client.application?.commands.fetch({ withLocalizations: true }); 18 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'); 25 } catch (error) { 26 logger.error('Error during ready event:', error); 27 }
+1 -1
src/handlers/initialzeCommands.ts
··· 33 await import(commandUrl) 34 ).default) as SlashCommandProps | RemindCommandProps; 35 if (!command.data) { 36 - console.log('No command data in file', `${cat}/${file}.. Skipping`); 37 continue; 38 } 39 command.category = cat;
··· 33 await import(commandUrl) 34 ).default) as SlashCommandProps | RemindCommandProps; 35 if (!command.data) { 36 + logger.warn('No command data in file', `${cat}/${file}.. Skipping`); 37 continue; 38 } 39 command.category = cat;
+27 -9
src/index.ts
··· 2 import e from 'express'; 3 import helmet from 'helmet'; 4 import cors from 'cors'; 5 - 6 import BotClient from './services/Client'; 7 import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './config'; 8 import rateLimit from 'express-rate-limit'; 9 import authenticateApiKey from './middlewares/verifyApiKey'; 10 import status from './routes/status'; 11 import authRoutes from './routes/auth'; 12 import todosRoutes from './routes/todos'; 13 import apiKeysRoutes from './routes/apiKeys'; 14 import remindersRoutes from './routes/reminders'; 15 import { resetOldStrikes } from './utils/userStrikes'; 16 import logger from './utils/logger'; 17 ··· 28 29 const app = e(); 30 const startTime = Date.now(); 31 32 app.use(helmet()); 33 app.use( ··· 75 76 const bot = new BotClient(); 77 bot.init(); 78 79 app.use('/api/auth', authRoutes); 80 app.use('/api/todos', todosRoutes); 81 app.use('/api/user/api-keys', apiKeysRoutes); 82 - app.use('/api/reminders', remindersRoutes); 83 84 app.use('/api/status', authenticateApiKey, status(bot)); 85 ··· 87 res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); 88 }); 89 90 - app.use(e.static('web/dist')); 91 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' }); 97 }); 98 99 setInterval( ··· 106 const server = app.listen(PORT, async () => { 107 logger.debug('Aethel is live on', `http://localhost:${PORT}`); 108 109 - const { sendDeploymentNotification } = await import('./utils/sendDeploymentNotification'); 110 await sendDeploymentNotification(startTime); 111 }); 112
··· 2 import e from 'express'; 3 import helmet from 'helmet'; 4 import cors from 'cors'; 5 + import path from 'path'; 6 + import { fileURLToPath } from 'url'; 7 import BotClient from './services/Client'; 8 import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './config'; 9 import rateLimit from 'express-rate-limit'; 10 import authenticateApiKey from './middlewares/verifyApiKey'; 11 + import { authenticateToken } from './middlewares/auth'; 12 import status from './routes/status'; 13 import authRoutes from './routes/auth'; 14 import todosRoutes from './routes/todos'; 15 import apiKeysRoutes from './routes/apiKeys'; 16 import remindersRoutes from './routes/reminders'; 17 + import voteWebhookRoutes from './routes/voteWebhook'; 18 import { resetOldStrikes } from './utils/userStrikes'; 19 import logger from './utils/logger'; 20 ··· 31 32 const app = e(); 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'); 37 38 app.use(helmet()); 39 app.use( ··· 81 82 const bot = new BotClient(); 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 + }); 94 95 app.use('/api/auth', authRoutes); 96 app.use('/api/todos', todosRoutes); 97 app.use('/api/user/api-keys', apiKeysRoutes); 98 + app.use('/api/reminders', authenticateToken, remindersRoutes); 99 + app.use('/api', voteWebhookRoutes); 100 101 app.use('/api/status', authenticateApiKey, status(bot)); 102 ··· 104 res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); 105 }); 106 107 + app.use(e.static(distPath, { index: false, maxAge: '1h' })); 108 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' }); 115 }); 116 117 setInterval( ··· 124 const server = app.listen(PORT, async () => { 125 logger.debug('Aethel is live on', `http://localhost:${PORT}`); 126 127 + const { sendDeploymentNotification } = await import('./utils/sendDeploymentNotification.js'); 128 await sendDeploymentNotification(startTime); 129 }); 130
+6 -20
src/middlewares/auth.ts
··· 2 import jwt from 'jsonwebtoken'; 3 import logger from '../utils/logger'; 4 5 - const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; 6 7 interface JwtPayload { 8 userId: string; ··· 22 } 23 24 try { 25 - const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; 26 req.user = decoded; 27 next(); 28 } catch (error) { ··· 37 return res.status(500).json({ error: 'Token verification failed' }); 38 } 39 }; 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 - };
··· 2 import jwt from 'jsonwebtoken'; 3 import logger from '../utils/logger'; 4 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; 10 11 interface JwtPayload { 12 userId: string; ··· 26 } 27 28 try { 29 + const decoded = jwt.verify(token, JWT_SECRET) as unknown as JwtPayload; 30 req.user = decoded; 31 next(); 32 } catch (error) { ··· 41 return res.status(500).json({ error: 'Token verification failed' }); 42 } 43 };
+1 -1
src/middlewares/verifyApiKey.ts
··· 1 import * as config from '@/config'; 2 import { RequestHandler } from 'express'; 3 4 - const authenticateApiKey: RequestHandler = (req, res, next) => { 5 const apiKey = req.headers['x-api-key']; 6 if (!apiKey || typeof apiKey !== 'string') { 7 res.status(401).json({ error: 'Unauthorized: Missing API key' });
··· 1 import * as config from '@/config'; 2 import { RequestHandler } from 'express'; 3 4 + export const authenticateApiKey: RequestHandler = (req, res, next) => { 5 const apiKey = req.headers['x-api-key']; 6 if (!apiKey || typeof apiKey !== 'string') { 7 res.status(401).json({ error: 'Unauthorized: Missing API key' });
+120 -26
src/routes/apiKeys.ts
··· 1 import { Router } from 'express'; 2 import pool from '../utils/pgClient'; 3 import logger from '../utils/logger'; 4 import { authenticateToken } from '../middlewares/auth'; ··· 11 'openrouter.ai', 12 'generativelanguage.googleapis.com', 13 'api.anthropic.com', 14 ]; 15 16 function getOpenAIClient(apiKey: string, baseURL?: string): OpenAI { ··· 250 }); 251 } 252 253 - const testModel = model || 'openai/gpt-4o-mini'; 254 - const client = getOpenAIClient(apiKey, fullApiUrl); 255 256 - try { 257 - const response = await client.chat.completions.create({ 258 - model: testModel, 259 - messages: [ 260 { 261 - role: 'user', 262 - content: 263 - 'Hello! This is a test message. Please respond with "API key test successful!"', 264 }, 265 - ], 266 - max_tokens: 50, 267 - temperature: 0.1, 268 - }); 269 270 - const testMessage = response.choices?.[0]?.message?.content || 'Test completed'; 271 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 - }); 284 } 285 } catch (error) { 286 logger.error('Error testing API key:', error);
··· 1 import { Router } from 'express'; 2 + import axios from 'axios'; 3 import pool from '../utils/pgClient'; 4 import logger from '../utils/logger'; 5 import { authenticateToken } from '../middlewares/auth'; ··· 12 'openrouter.ai', 13 'generativelanguage.googleapis.com', 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', 24 ]; 25 26 function getOpenAIClient(apiKey: string, baseURL?: string): OpenAI { ··· 260 }); 261 } 262 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 + }); 273 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, 295 { 296 + contents: [ 297 + { 298 + role: 'user', 299 + parts: [ 300 + { 301 + text: testPrompt, 302 + }, 303 + ], 304 + }, 305 + ], 306 }, 307 + { 308 + headers: { 'Content-Type': 'application/json' }, 309 + timeout: 10000, 310 + }, 311 + ); 312 313 + const testMessage = 314 + response.data?.candidates?.[0]?.content?.parts?.[0]?.text || 'Test completed'; 315 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 + } 378 } 379 } catch (error) { 380 logger.error('Error testing API key:', error);
+6 -7
src/routes/auth.ts
··· 6 7 const router = Router(); 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'; 13 const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; 14 - const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:2020'; 15 16 interface DiscordUser { 17 id: string; ··· 22 } 23 24 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`; 26 res.redirect(discordAuthUrl); 27 }); 28 ··· 50 client_secret: DISCORD_CLIENT_SECRET!, 51 grant_type: 'authorization_code', 52 code: code as string, 53 - redirect_uri: DISCORD_REDIRECT_URI, 54 }), 55 }); 56
··· 6 7 const router = Router(); 8 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; 12 const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; 13 + const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000'; 14 15 interface DiscordUser { 16 id: string; ··· 21 } 22 23 router.get('/discord', (req, res) => { 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`; 25 res.redirect(discordAuthUrl); 26 }); 27 ··· 49 client_secret: DISCORD_CLIENT_SECRET!, 50 grant_type: 'authorization_code', 51 code: code as string, 52 + redirect_uri: DISCORD_REDIRECT_URI!, 53 }), 54 }); 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 public t = new Collection<string, any>(); 23 public socialMediaManager?: SocialMediaManager; 24 25 constructor() { 26 super({ ··· 41 }, 42 }); 43 BotClient.instance = this; 44 } 45 46 public static getInstance(): BotClient | null { ··· 56 } 57 58 private async setupEvents() { 59 - console.log('Initializing events...'); 60 const eventsDir = path.join(srcDir, 'events'); 61 for (const event of readdirSync(path.join(eventsDir))) { 62 const filepath = path.join(eventsDir, event); ··· 127 128 const shutdown = async (signal?: NodeJS.Signals) => { 129 try { 130 - console.log(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); 131 await this.socialMediaManager?.cleanup(); 132 await pool.end(); 133 - console.log('Database pool closed. Exiting.'); 134 } catch (e) { 135 console.error('Error during graceful shutdown:', e); 136 } finally {
··· 21 // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 public t = new Collection<string, any>(); 23 public socialMediaManager?: SocialMediaManager; 24 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 + public lastModalRawByUser: Map<string, any> = new Map(); 26 27 constructor() { 28 super({ ··· 43 }, 44 }); 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 + }); 82 } 83 84 public static getInstance(): BotClient | null { ··· 94 } 95 96 private async setupEvents() { 97 + logger.info('Initializing events...'); 98 const eventsDir = path.join(srcDir, 'events'); 99 for (const event of readdirSync(path.join(eventsDir))) { 100 const filepath = path.join(eventsDir, event); ··· 165 166 const shutdown = async (signal?: NodeJS.Signals) => { 167 try { 168 + logger.info(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); 169 await this.socialMediaManager?.cleanup(); 170 await pool.end(); 171 + logger.info('Database pool closed. Exiting.'); 172 } catch (e) { 173 console.error('Error during graceful shutdown:', e); 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 export interface RandomReddit { 2 response_time_ms: number; 3 - source: 'reddit'; 4 subreddit: string; 5 title: string; 6 - upvotes: 71; 7 url: string; 8 } 9
··· 1 export interface RandomReddit { 2 response_time_ms: number; 3 + source: string; 4 subreddit: string; 5 title: string; 6 + upvotes: number; 7 url: string; 8 } 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 } 132 } 133 134 - function canDecrypt(encrypted: string): boolean { 135 - try { 136 - decrypt(encrypted); 137 - return true; 138 - } catch { 139 - return false; 140 - } 141 - } 142 - 143 function isValidEncryptedFormat(encrypted: string): boolean { 144 if (!encrypted || typeof encrypted !== 'string') { 145 return false; ··· 162 } 163 } 164 165 - export { encrypt, decrypt, canDecrypt, isValidEncryptedFormat, EncryptionError };
··· 131 } 132 } 133 134 function isValidEncryptedFormat(encrypted: string): boolean { 135 if (!encrypted || typeof encrypted !== 'string') { 136 return false; ··· 153 } 154 } 155 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 } 41 42 initializeGitCommitHash().catch((error) => { 43 - console.warn('Failed to initialize git commit hash:', error.message); 44 }); 45 46 export default getGitCommitHash;
··· 40 } 41 42 initializeGitCommitHash().catch((error) => { 43 + logger.warn('Failed to initialize git commit hash:', error.message); 44 }); 45 46 export default getGitCommitHash;
+11 -4
src/utils/logger.ts
··· 58 new winston.transports.Console({ 59 format: winston.format.combine( 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 - }), 65 ), 66 }), 67 ],
··· 58 new winston.transports.Console({ 59 format: winston.format.combine( 60 winston.format.colorize(), 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 + ), 72 ), 73 }), 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 return array[Math.floor(Math.random() * array.length)]; 6 } 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 export function iso2ToDiscordFlag(iso2: string): string { 18 if (!iso2 || iso2.length !== 2) return ''; 19 return `:flag_${iso2.toLowerCase()}:`;
··· 5 return array[Math.floor(Math.random() * array.length)]; 6 } 7 8 export function iso2ToDiscordFlag(iso2: string): string { 9 if (!iso2 || iso2.length !== 2) return ''; 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 5 function getGitInfo() { 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(); 10 const branch = 11 process.env.GIT_BRANCH || 12 process.env.VERCEL_GIT_COMMIT_REF || 13 process.env.COOLIFY_BRANCH || 14 - execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); 15 16 return { 17 - commitHash: commitHash.substring(0, 7), 18 commitMessage, 19 branch, 20 };
··· 4 5 function getGitInfo() { 6 try { 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 + } 84 const branch = 85 process.env.GIT_BRANCH || 86 process.env.VERCEL_GIT_COMMIT_REF || 87 process.env.COOLIFY_BRANCH || 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 + })(); 96 97 return { 98 + commitHash: shortHash, 99 commitMessage, 100 branch, 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 } 17 } 18 19 - export async function getUserStrikeInfo(userId: string): Promise<StrikeInfo | null> { 20 if (!userId || typeof userId !== 'string') { 21 throw new StrikeError('Invalid user ID provided'); 22 } ··· 152 throw new StrikeError('Failed to reset old strikes'); 153 } 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 - }
··· 16 } 17 } 18 19 + async function _getUserStrikeInfo(userId: string): Promise<StrikeInfo | null> { 20 if (!userId || typeof userId !== 'string') { 21 throw new StrikeError('Invalid user ID provided'); 22 } ··· 152 throw new StrikeError('Failed to reset old strikes'); 153 } 154 }
+29 -22
src/utils/validation.ts
··· 118 return validator.isFQDN(domain, { require_tld: true }); 119 } 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') 129 .replace(/[5$]/g, 's') 130 - .replace(/[7]/g, 't'); 131 - return normalized; 132 } 133 134 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 - } 147 } 148 } 149 } 150 return null; 151 } 152
··· 118 return validator.isFQDN(domain, { require_tld: true }); 119 } 120 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') 133 .replace(/[5$]/g, 's') 134 + .replace(/7/g, 't') 135 + .trim(); 136 } 137 138 export function getUnallowedWordCategory(text: string): string | null { 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; 153 } 154 } 155 } 156 + 157 return null; 158 } 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 import { toast } from 'sonner'; 3 4 const api = axios.create({ 5 - baseURL: `${import.meta.env.VITE_FRONTEND_URL}/api`, 6 timeout: 10000, 7 }); 8 ··· 61 deleteApiKey: () => api.delete('/user/api-keys'), 62 testApiKey: (data: { apiKey: string; model?: string; apiUrl?: string }) => 63 api.post('/user/api-keys/test', data), 64 }; 65 66 export const remindersAPI = {
··· 2 import { toast } from 'sonner'; 3 4 const api = axios.create({ 5 + baseURL: `/api`, 6 timeout: 10000, 7 }); 8 ··· 61 deleteApiKey: () => api.delete('/user/api-keys'), 62 testApiKey: (data: { apiKey: string; model?: string; apiUrl?: string }) => 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 + }, 91 }; 92 93 export const remindersAPI = {
+387 -168
web/src/pages/ApiKeysPage.tsx
··· 1 - import { useState } from 'react'; 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 3 - import { Key, Eye, EyeOff, TestTube, Save, Trash2, AlertCircle, CheckCircle } from 'lucide-react'; 4 import { toast } from 'sonner'; 5 import { apiKeysAPI } from '../lib/api'; 6 7 const ApiKeysPage = () => { 8 const [showApiKey, setShowApiKey] = useState(false); 9 - const [formData, setFormData] = useState({ 10 apiKey: '', 11 model: '', 12 - apiUrl: '', 13 }); 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 19 - const { data: apiKeyInfo, isLoading } = useQuery({ 20 - queryKey: ['api-keys'], 21 - queryFn: () => apiKeysAPI.getApiKeys().then((res) => res.data), 22 - }); 23 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 - }); 37 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 - }); 50 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 - }); 66 67 - const handleSubmit = (e: React.FormEvent) => { 68 - e.preventDefault(); 69 - if (!formData.apiKey.trim()) { 70 - toast.error('API key is required'); 71 - return; 72 } 73 - if (!hasPassedTest) { 74 - toast.error('Please test the API key before saving'); 75 - return; 76 } 77 - updateApiKeyMutation.mutate({ 78 - apiKey: formData.apiKey, 79 - model: formData.model || undefined, 80 - apiUrl: formData.apiUrl || undefined, 81 - }); 82 }; 83 84 - const handleTest = () => { 85 - if (!formData.apiKey.trim()) { 86 - toast.error('API key is required for testing'); 87 return; 88 } 89 - setTestResult(null); 90 - testApiKeyMutation.mutate({ 91 - apiKey: formData.apiKey, 92 - model: formData.model || undefined, 93 - apiUrl: formData.apiUrl || undefined, 94 - }); 95 }; 96 97 const handleEdit = () => { 98 setIsEditing(true); 99 setFormData({ 100 - apiKey: '', 101 model: apiKeyInfo?.model || '', 102 - apiUrl: apiKeyInfo?.apiUrl || '', 103 }); 104 setTestResult(null); 105 setHasPassedTest(false); ··· 107 108 const handleCancel = () => { 109 setIsEditing(false); 110 - setFormData({ apiKey: '', model: '', apiUrl: '' }); 111 setTestResult(null); 112 setHasPassedTest(false); 113 }; 114 115 if (isLoading) { 116 return ( 117 <div className="flex items-center justify-center h-64"> ··· 216 <span className="text-red-600 dark:text-red-400">*</span> You must test the API key 217 before saving to ensure it works correctly. 218 </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} 226 onChange={(e) => { 227 - setFormData({ ...formData, apiKey: e.target.value }); 228 setHasPassedTest(false); 229 setTestResult(null); 230 }} 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" 239 > 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> 246 </div> 247 </div> 248 249 - <div> 250 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 251 Model (Optional) 252 </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> 288 </div> 289 290 {testResult && ( ··· 304 </div> 305 )} 306 307 - <div className="flex flex-col sm:flex-row sm:justify-between gap-3 pt-4"> 308 <button 309 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" 313 > 314 - <TestTube className="h-4 w-4 mr-2" /> 315 - {testApiKeyMutation.isPending ? 'Testing...' : 'Test API Key'} 316 </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 </div> 345 </form> 346 </div>
··· 1 + import React, { useState, useEffect } from 'react'; 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 3 + import { 4 + Key, 5 + Eye, 6 + EyeOff, 7 + TestTube, 8 + Save, 9 + Trash2, 10 + AlertCircle, 11 + CheckCircle, 12 + Loader2, 13 + } from 'lucide-react'; 14 import { toast } from 'sonner'; 15 import { apiKeysAPI } from '../lib/api'; 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 + 35 const ApiKeysPage = () => { 36 const [showApiKey, setShowApiKey] = useState(false); 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>({ 45 apiKey: '', 46 model: '', 47 + apiUrl: 'https://api.openai.com/v1', 48 }); 49 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]); 55 56 + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { 57 + const { name, value } = e.target; 58 + setFormData((prev) => ({ 59 + ...prev, 60 + [name]: value, 61 + })); 62 + }; 63 64 + const handleModelSelect = (model: string) => { 65 + setFormData((prev) => ({ 66 + ...prev, 67 + model, 68 + })); 69 + setModelSearch(''); 70 + setShowModelDropdown(false); 71 + }; 72 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); 80 81 + if (availableModels.length === 0 && formData.apiKey) { 82 + fetchModels(); 83 } 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); 106 } 107 }; 108 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 + }); 118 return; 119 } 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 + } 138 }; 139 140 const handleEdit = () => { 141 setIsEditing(true); 142 setFormData({ 143 + apiKey: apiKeyInfo?.apiKey || '', 144 model: apiKeyInfo?.model || '', 145 + apiUrl: apiKeyInfo?.apiUrl || 'https://api.openai.com/v1', 146 }); 147 setTestResult(null); 148 setHasPassedTest(false); ··· 150 151 const handleCancel = () => { 152 setIsEditing(false); 153 + setFormData({ 154 + apiKey: apiKeyInfo?.apiKey || '', 155 + model: apiKeyInfo?.model || '', 156 + apiUrl: apiKeyInfo?.apiUrl || 'https://api.openai.com/v1', 157 + }); 158 setTestResult(null); 159 setHasPassedTest(false); 160 }; 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 + 265 if (isLoading) { 266 return ( 267 <div className="flex items-center justify-center h-64"> ··· 366 <span className="text-red-600 dark:text-red-400">*</span> You must test the API key 367 before saving to ensure it works correctly. 368 </p> 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} 376 onChange={(e) => { 377 + const url = e.target.value; 378 + setFormData({ ...formData, apiUrl: url, model: '' }); 379 + setAvailableModels([]); 380 setHasPassedTest(false); 381 setTestResult(null); 382 }} 383 + className="input dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 w-full" 384 > 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> 449 </div> 450 </div> 451 452 + <div className="relative"> 453 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> 454 Model (Optional) 455 </label> 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> 502 </div> 503 504 {testResult && ( ··· 518 </div> 519 )} 520 521 + <div className="flex flex-col sm:flex-row justify-end gap-3 pt-4"> 522 <button 523 type="button" 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 + }`} 540 > 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 + )} 562 </button> 563 </div> 564 </form> 565 </div>
+1 -1
web/src/pages/LandingPage.tsx
··· 205 <div className="mb-4"> 206 <p className="text-xs text-gray-600 dark:text-gray-400 mb-2">Powered by</p> 207 <a 208 - href="https://royalehosting.net?utm_source=aethel.xyz&utm_medium=referral&utm_campaign=powered_by&utm_content=footer" 209 target="_blank" 210 rel="noopener noreferrer" 211 className="inline-block hover:opacity-80 transition-opacity"
··· 205 <div className="mb-4"> 206 <p className="text-xs text-gray-600 dark:text-gray-400 mb-2">Powered by</p> 207 <a 208 + href="https://royalehosting.net/?aff=8033?utm_source=aethel.xyz&utm_medium=referral&utm_campaign=powered_by&utm_content=footer" 209 target="_blank" 210 rel="noopener noreferrer" 211 className="inline-block hover:opacity-80 transition-opacity"
+2 -2
web/src/pages/LoginPage.tsx
··· 28 29 const handleDiscordLogin = async () => { 30 try { 31 - window.location.href = `${import.meta.env.VITE_FRONTEND_URL}/api/auth/discord`; 32 } catch (_error) { 33 toast.error('Failed to initiate Discord login'); 34 } ··· 66 67 <button 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" 70 > 71 <svg 72 className="w-5 h-5 mr-3"
··· 28 29 const handleDiscordLogin = async () => { 30 try { 31 + window.location.href = `/api/auth/discord`; 32 } catch (_error) { 33 toast.error('Failed to initiate Discord login'); 34 } ··· 66 67 <button 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 hover:shadow-xl" 70 > 71 <svg 72 className="w-5 h-5 mr-3"
+1 -1
web/src/pages/PrivacyPage.tsx
··· 178 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 179 We implement the following security measures to protect your information: 180 </p> 181 - <ul className="list-disc pl-6 space-y-3 text-gray-700 mt-2"> 182 <li> 183 <strong>API Key Encryption:</strong> AES-256-GCM encryption for all stored API keys 184 </li>
··· 178 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 179 We implement the following security measures to protect your information: 180 </p> 181 + <ul className="list-disc pl-6 space-y-3 text-gray-700 dark:text-gray-300 mt-2"> 182 <li> 183 <strong>API Key Encryption:</strong> AES-256-GCM encryption for all stored API keys 184 </li>
+44 -7
web/src/pages/TermsPage.tsx
··· 4 return ( 5 <LegalLayout 6 title="Terms of Service" 7 - lastUpdated="June 16, 2025" 8 > 9 <div className="space-y-8"> 10 <section> ··· 32 <li>Random cat and dog images</li> 33 <li>Weather information</li> 34 <li>Wiki lookups</li> 35 <li>And other Discord utilities</li> 36 </ul> 37 </section> ··· 54 55 <section> 56 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 57 - 4. API Usage 58 </h2> 59 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 60 The Bot may use third-party APIs and services (&quot;Third-Party Services&quot;). Your ··· 63 <ul className="list-disc pl-6 space-y-3 text-gray-700 dark:text-gray-300 mt-2"> 64 <li>You are responsible for the security of your API keys</li> 65 <li> 66 - We do not store your API keys permanently - they are only kept in memory during your 67 - active session 68 </li> 69 <li> 70 You must comply with the terms of service of any third-party APIs you use with the Bot 71 </li> ··· 78 79 <section> 80 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 81 - 5. Limitation of Liability 82 </h2> 83 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 84 The Bot is provided &quot;as is&quot; without any warranties. We are not responsible for ··· 89 90 <section> 91 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 92 - 6. Changes to Terms 93 </h2> 94 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 95 We reserve the right to modify these terms at any time. Continued use of the Bot after ··· 99 100 <section> 101 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 102 - 7. Contact 103 </h2> 104 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 105 If you have any questions about these Terms of Service, please contact us at{' '}
··· 4 return ( 5 <LegalLayout 6 title="Terms of Service" 7 + lastUpdated="November 9, 2025" 8 > 9 <div className="space-y-8"> 10 <section> ··· 32 <li>Random cat and dog images</li> 33 <li>Weather information</li> 34 <li>Wiki lookups</li> 35 + <li>Informational stock snapshots</li> 36 <li>And other Discord utilities</li> 37 </ul> 38 </section> ··· 55 56 <section> 57 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 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 94 </h2> 95 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 96 The Bot may use third-party APIs and services (&quot;Third-Party Services&quot;). Your ··· 99 <ul className="list-disc pl-6 space-y-3 text-gray-700 dark:text-gray-300 mt-2"> 100 <li>You are responsible for the security of your API keys</li> 101 <li> 102 + Your API keys are stored securely using industry-standard encryption and are only 103 + accessible to you 104 </li> 105 + <li>You can delete your API keys at any time through the API keys management page</li> 106 <li> 107 You must comply with the terms of service of any third-party APIs you use with the Bot 108 </li> ··· 115 116 <section> 117 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 118 + 6. Limitation of Liability 119 </h2> 120 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 121 The Bot is provided &quot;as is&quot; without any warranties. We are not responsible for ··· 126 127 <section> 128 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 129 + 7. Changes to Terms 130 </h2> 131 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 132 We reserve the right to modify these terms at any time. Continued use of the Bot after ··· 136 137 <section> 138 <h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-200 mb-4"> 139 + 8. Contact 140 </h2> 141 <p className="text-gray-700 dark:text-gray-300 leading-relaxed"> 142 If you have any questions about these Terms of Service, please contact us at{' '}
+1 -1
web/src/stores/authStore.ts
··· 47 48 try { 49 axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 50 - const response = await axios.get(`${import.meta.env.VITE_FRONTEND_URL}/api/auth/me`); 51 set({ 52 token, 53 user: response.data.user,
··· 47 48 try { 49 axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 50 + const response = await axios.get(`/api/auth/me`); 51 set({ 52 token, 53 user: response.data.user,
-2
web/src/vite-env.d.ts
··· 2 3 interface ImportMetaEnv { 4 readonly VITE_STATUS_API_KEY: string; 5 - readonly VITE_FRONTEND_URL: string; 6 - readonly VITE_DISCORD_CLIENT_ID: string; 7 } 8 9 interface ImportMeta {
··· 2 3 interface ImportMetaEnv { 4 readonly VITE_STATUS_API_KEY: string; 5 } 6 7 interface ImportMeta {