+13
-6
.env.example
+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
.github/FUNDING.yml
···
···
1
+
ko_fi: scanash
+2
-2
.github/workflows/pr.yml
+2
-2
.github/workflows/pr.yml
+2
-10
Dockerfile
+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
+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
+215
-50
bun.lock
+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
+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
+4
environment.d.ts
···
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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+1
-1
scripts/run-migrations.js
+1
-1
src/commands/fun/cat.ts
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+1
-1
src/handlers/initialzeCommands.ts
+27
-9
src/index.ts
+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
+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
-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
+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
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
+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
+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
+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
-2
src/types/base.ts
+2
-2
src/types/base.ts
+224
src/types/componentsV2.ts
+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
+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
+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 };
-82
src/utils/encryption.ts
-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
+1
-1
src/utils/getGitCommitHash.ts
+11
-4
src/utils/logger.ts
+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
+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
-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()}:`;
+31
src/utils/rateLimiter.ts
+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
+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
+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
+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
+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
-
}
+29
-22
src/utils/validation.ts
+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
+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
+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
+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
+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
+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
+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
+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 ("Third-Party Services"). 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 "as is" 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 & /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 ("Third-Party Services"). 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 "as is" 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
+1
-1
web/src/stores/authStore.ts
-2
web/src/vite-env.d.ts
-2
web/src/vite-env.d.ts