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