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

add stocks

Changed files
+1149 -1
locales
src
commands
utilities
config
events
services
utils
+5 -1
.env.example
··· 13 13 OPENROUTER_API_KEY= 14 14 # OpenWeather API key for the weather command 15 15 OPENWEATHER_API_KEY= 16 + # Massive.com API key for the /stocks command 17 + MASSIVE_API_KEY= 18 + # Optional override for the Massive.com REST base URL 19 + MASSIVE_API_BASE_URL=https://api.massive.com 16 20 # Log level for the application (debug, info, warn, error) 17 21 LOG_LEVEL=info 18 22 # Node environment (development, production) ··· 30 34 TOPGG_WEBHOOK_SECRET= 31 35 TOPGG_WEBHOOK_AUTH= 32 36 # Top.gg token to check vote status 33 - TOPGG_TOKEN= 37 + TOPGG_TOKEN=
+120
bun.lock
··· 7 7 "@atproto/identity": "^0.4.9", 8 8 "@discordjs/rest": "^2.6.0", 9 9 "@fedify/fedify": "^1.8.12", 10 + "@massive.com/client-js": "^9.0.0", 10 11 "@types/he": "^1.2.3", 11 12 "@types/sanitize-html": "^2.16.0", 12 13 "axios": "^1.12.2", 14 + "canvas": "^3.2.0", 13 15 "city-timezones": "^1.3.2", 14 16 "concurrently": "^9.2.1", 15 17 "cors": "^2.8.5", ··· 174 176 175 177 "@logtape/logtape": ["@logtape/logtape@1.0.4", "", {}, "sha512-YvNVrXIxVpnY528zoiEjX8PqTfr0UCtKXyssvaWL8AE+OByFTCooKuKMdPlm6g65YUI9fPXrHn4UnogSskABnA=="], 176 178 179 + "@massive.com/client-js": ["@massive.com/client-js@9.0.0", "", { "dependencies": { "axios": "^1.8.4", "cross-fetch": "^3.1.4", "query-string": "^7.0.1", "websocket": "^1.0.34" } }, "sha512-vfOSVMp7uIfFgsyyX58sMZvcwS67psOTbRTox+7ahKMsG7aztFTJfoBwmjj5EjkX4Ise1BB1OEh73fbGzsj+xA=="], 180 + 177 181 "@multiformats/base-x": ["@multiformats/base-x@4.0.1", "", {}, "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw=="], 178 182 179 183 "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], ··· 304 308 305 309 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 306 310 311 + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 312 + 307 313 "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], 314 + 315 + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], 308 316 309 317 "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], 310 318 ··· 314 322 315 323 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 316 324 325 + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], 326 + 317 327 "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], 328 + 329 + "bufferutil": ["bufferutil@4.0.9", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw=="], 318 330 319 331 "byte-encodings": ["byte-encodings@1.0.11", "", {}, "sha512-+/xR2+ySc2yKGtud3DGkGSH1DNwHfRVK0KTnMhoeH36/KwG+tHQ4d9B3jxJFq7dW27YcfudkywaYJRPA2dmxzg=="], 320 332 ··· 334 346 335 347 "canonicalize": ["canonicalize@1.0.8", "", {}, "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A=="], 336 348 349 + "canvas": ["canvas@3.2.0", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.3" } }, "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA=="], 350 + 337 351 "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 338 352 339 353 "change-case": ["change-case@3.1.0", "", { "dependencies": { "camel-case": "^3.0.0", "constant-case": "^2.0.0", "dot-case": "^2.1.0", "header-case": "^1.0.0", "is-lower-case": "^1.1.0", "is-upper-case": "^1.1.0", "lower-case": "^1.1.1", "lower-case-first": "^1.0.0", "no-case": "^2.3.2", "param-case": "^2.1.0", "pascal-case": "^2.0.0", "path-case": "^2.1.0", "sentence-case": "^2.1.0", "snake-case": "^2.1.0", "swap-case": "^1.1.0", "title-case": "^2.1.0", "upper-case": "^1.1.1", "upper-case-first": "^1.1.0" } }, "sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw=="], ··· 346 360 347 361 "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], 348 362 363 + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], 364 + 349 365 "city-timezones": ["city-timezones@1.3.2", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-XztdL/2EWpfmgRIOzrKVOWFp6VdmaD9FNTZPINlez1etIn0mMNn01RMmSfOp6LUP/h1M2ZLX80N1O+WKwhzC+w=="], 350 366 351 367 "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], ··· 380 396 381 397 "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], 382 398 399 + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], 400 + 383 401 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 384 402 385 403 "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], 386 404 387 405 "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], 406 + 407 + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], 388 408 389 409 "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], 390 410 ··· 392 412 393 413 "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], 394 414 415 + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], 416 + 417 + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], 418 + 395 419 "dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="], 420 + 421 + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], 396 422 397 423 "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 398 424 ··· 403 429 "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 404 430 405 431 "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], 432 + 433 + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 406 434 407 435 "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], 408 436 ··· 435 463 "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], 436 464 437 465 "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], 466 + 467 + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], 438 468 439 469 "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 440 470 ··· 448 478 449 479 "es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="], 450 480 481 + "es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="], 482 + 483 + "es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="], 484 + 485 + "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], 486 + 451 487 "esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="], 452 488 453 489 "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], ··· 465 501 "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], 466 502 467 503 "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 504 + 505 + "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], 468 506 469 507 "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], 470 508 ··· 478 516 479 517 "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 480 518 519 + "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], 520 + 481 521 "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], 482 522 523 + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], 524 + 483 525 "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], 484 526 485 527 "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], 486 528 487 529 "express-validator": ["express-validator@7.2.1", "", { "dependencies": { "lodash": "^4.17.21", "validator": "~13.12.0" } }, "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ=="], 530 + 531 + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], 488 532 489 533 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 490 534 ··· 506 550 507 551 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 508 552 553 + "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], 554 + 509 555 "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], 510 556 511 557 "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], ··· 526 572 527 573 "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], 528 574 575 + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], 576 + 529 577 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 530 578 531 579 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], ··· 538 586 539 587 "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], 540 588 589 + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], 590 + 541 591 "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 542 592 543 593 "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], ··· 570 620 571 621 "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], 572 622 623 + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 624 + 573 625 "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 574 626 575 627 "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], ··· 580 632 581 633 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 582 634 635 + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], 636 + 583 637 "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], 584 638 585 639 "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], ··· 601 655 "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], 602 656 603 657 "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], 658 + 659 + "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], 604 660 605 661 "is-upper-case": ["is-upper-case@1.1.2", "", { "dependencies": { "upper-case": "^1.1.0" } }, "sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw=="], 606 662 ··· 690 746 691 747 "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 692 748 749 + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], 750 + 693 751 "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 694 752 695 753 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 754 + 755 + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], 696 756 697 757 "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], 698 758 ··· 708 768 709 769 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 710 770 771 + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], 772 + 711 773 "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 712 774 713 775 "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], 714 776 777 + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], 778 + 715 779 "no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="], 716 780 781 + "node-abi": ["node-abi@3.80.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA=="], 782 + 783 + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], 784 + 717 785 "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], 718 786 719 787 "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], 788 + 789 + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], 720 790 721 791 "nodemon": ["nodemon@3.1.10", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw=="], 722 792 ··· 729 799 "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], 730 800 731 801 "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 802 + 803 + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 732 804 733 805 "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], 734 806 ··· 804 876 805 877 "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], 806 878 879 + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], 880 + 807 881 "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 808 882 809 883 "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], ··· 816 890 817 891 "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], 818 892 893 + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], 894 + 819 895 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 820 896 821 897 "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], ··· 823 899 "pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="], 824 900 825 901 "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], 902 + 903 + "query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], 826 904 827 905 "queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="], 828 906 ··· 831 909 "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], 832 910 833 911 "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], 912 + 913 + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], 834 914 835 915 "rdf-canonize": ["rdf-canonize@3.4.0", "", { "dependencies": { "setimmediate": "^1.0.5" } }, "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA=="], 836 916 ··· 888 968 889 969 "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 890 970 971 + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], 972 + 973 + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], 974 + 891 975 "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], 892 976 893 977 "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], ··· 902 986 903 987 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 904 988 989 + "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], 990 + 905 991 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 906 992 907 993 "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], ··· 910 996 911 997 "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 912 998 999 + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], 1000 + 913 1001 "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 914 1002 915 1003 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], ··· 928 1016 929 1017 "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], 930 1018 1019 + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], 1020 + 1021 + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], 1022 + 931 1023 "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], 932 1024 933 1025 "title-case": ["title-case@2.1.1", "", { "dependencies": { "no-case": "^2.2.0", "upper-case": "^1.0.3" } }, "sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q=="], ··· 937 1029 "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 938 1030 939 1031 "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="], 1032 + 1033 + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], 940 1034 941 1035 "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], 942 1036 ··· 953 1047 "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 954 1048 955 1049 "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], 1050 + 1051 + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], 1052 + 1053 + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], 956 1054 957 1055 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 958 1056 959 1057 "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], 960 1058 1059 + "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], 1060 + 961 1061 "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 962 1062 963 1063 "typescript-eslint": ["typescript-eslint@8.44.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.44.1", "@typescript-eslint/parser": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/utils": "8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg=="], ··· 986 1086 987 1087 "urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="], 988 1088 1089 + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], 1090 + 989 1091 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 990 1092 991 1093 "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], ··· 1000 1102 1001 1103 "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], 1002 1104 1105 + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], 1106 + 1107 + "websocket": ["websocket@1.0.35", "", { "dependencies": { "bufferutil": "^4.0.1", "debug": "^2.2.0", "es5-ext": "^0.10.63", "typedarray-to-buffer": "^3.1.5", "utf-8-validate": "^5.0.2", "yaeti": "^0.0.6" } }, "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q=="], 1108 + 1003 1109 "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], 1004 1110 1005 1111 "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], 1112 + 1113 + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], 1006 1114 1007 1115 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1008 1116 ··· 1020 1128 1021 1129 "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 1022 1130 1131 + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 1132 + 1023 1133 "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], 1024 1134 1025 1135 "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], 1026 1136 1027 1137 "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 1138 + 1139 + "yaeti": ["yaeti@0.0.6", "", {}, "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug=="], 1028 1140 1029 1141 "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], 1030 1142 ··· 1084 1196 1085 1197 "concurrently/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], 1086 1198 1199 + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], 1200 + 1087 1201 "discord.js/@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], 1088 1202 1089 1203 "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], ··· 1098 1212 1099 1213 "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], 1100 1214 1215 + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], 1216 + 1101 1217 "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 1102 1218 1103 1219 "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], 1220 + 1221 + "websocket/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 1104 1222 1105 1223 "whois/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], 1106 1224 ··· 1139 1257 "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1140 1258 1141 1259 "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1260 + 1261 + "websocket/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1142 1262 1143 1263 "whois/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], 1144 1264
+2
environment.d.ts
··· 6 6 DATABASE_URL: string; 7 7 OPENROUTER_API_KEY: string; 8 8 OPENWEATHER_API_KEY: string; 9 + MASSIVE_API_KEY: string; 10 + MASSIVE_API_BASE_URL: string; 9 11 SOURCE_COMMIT: string; 10 12 STATUS_API_KEY: string; 11 13 TOKEN: string;
+37
locales/en-US.json
··· 92 92 "nolocation": "Location not found. Please check the city name and try again.", 93 93 "apikeymissing": "OpenWeather API key is missing or invalid." 94 94 }, 95 + "stocks": { 96 + "name": "stocks", 97 + "description": "Track stock prices and view quick charts", 98 + "option": { 99 + "ticker": { 100 + "name": "ticker", 101 + "description": "The stock ticker symbol (e.g., AAPL, TSLA)" 102 + }, 103 + "range": { 104 + "name": "range", 105 + "description": "Initial timeframe for the chart" 106 + } 107 + }, 108 + "errors": { 109 + "noapikey": "The Massive.com API key is missing. Ask the bot owner to configure MASSIVE_API_KEY.", 110 + "notfound": "I couldn't find any data for {ticker}. Please double-check the symbol.", 111 + "unauthorized": "Only the person who used /stocks can interact with these buttons." 112 + }, 113 + "labels": { 114 + "price": "Price", 115 + "change": "Change", 116 + "dayrange": "Day range", 117 + "volume": "Volume", 118 + "prevclose": "Prev close", 119 + "marketcap": "Market cap", 120 + "nochart": "Chart data unavailable for this timeframe." 121 + }, 122 + "buttons": { 123 + "timeframes": { 124 + "1d": "1D", 125 + "5d": "5D", 126 + "1m": "1M", 127 + "3m": "3M", 128 + "1y": "1Y" 129 + } 130 + } 131 + }, 95 132 "joke": { 96 133 "name": "joke", 97 134 "description": "Get a random joke!",
+2
package.json
··· 21 21 "@atproto/identity": "^0.4.9", 22 22 "@discordjs/rest": "^2.6.0", 23 23 "@fedify/fedify": "^1.8.12", 24 + "@massive.com/client-js": "^9.0.0", 24 25 "@types/he": "^1.2.3", 25 26 "@types/sanitize-html": "^2.16.0", 26 27 "axios": "^1.12.2", 28 + "canvas": "^3.2.0", 27 29 "city-timezones": "^1.3.2", 28 30 "concurrently": "^9.2.1", 29 31 "cors": "^2.8.5",
+424
src/commands/utilities/stocks.ts
··· 1 + import { 2 + SlashCommandBuilder, 3 + MessageFlags, 4 + InteractionContextType, 5 + ApplicationIntegrationType, 6 + EmbedBuilder, 7 + ActionRowBuilder, 8 + ButtonBuilder, 9 + ButtonStyle, 10 + AttachmentBuilder, 11 + type MessageActionRowComponentBuilder, 12 + } from 'discord.js'; 13 + import { SlashCommandProps } from '@/types/command'; 14 + import logger from '@/utils/logger'; 15 + import { sanitizeInput } from '@/utils/validation'; 16 + import { 17 + getTickerOverview, 18 + getAggregateSeries, 19 + buildBrandingUrl, 20 + sanitizeTickerInput, 21 + StockTimeframe, 22 + } from '@/services/massive'; 23 + import { renderStockCandles } from '@/utils/stockChart'; 24 + import { 25 + createCooldownManager, 26 + checkCooldown, 27 + setCooldown, 28 + createCooldownResponse, 29 + } from '@/utils/cooldown'; 30 + import { createCommandLogger } from '@/utils/commandLogger'; 31 + import { createErrorHandler } from '@/utils/errorHandler'; 32 + import * as config from '@/config'; 33 + import BotClient from '@/services/Client'; 34 + 35 + const cooldownManager = createCooldownManager('stocks', 5000); 36 + const commandLogger = createCommandLogger('stocks'); 37 + const errorHandler = createErrorHandler('stocks'); 38 + 39 + const DEFAULT_TIMEFRAME: StockTimeframe = '1d'; 40 + const SUPPORTED_TIMEFRAMES: StockTimeframe[] = ['1d', '5d', '1m', '3m', '1y']; 41 + const BUTTON_PREFIX = 'stocks_tf'; 42 + const MAX_DESCRIPTION_LENGTH = 350; 43 + 44 + const TIMEFRAME_LABEL_KEYS: Record<StockTimeframe, string> = { 45 + '1d': 'commands.stocks.buttons.timeframes.1d', 46 + '5d': 'commands.stocks.buttons.timeframes.5d', 47 + '1m': 'commands.stocks.buttons.timeframes.1m', 48 + '3m': 'commands.stocks.buttons.timeframes.3m', 49 + '1y': 'commands.stocks.buttons.timeframes.1y', 50 + }; 51 + 52 + const compactNumber = new Intl.NumberFormat('en-US', { 53 + notation: 'compact', 54 + maximumFractionDigits: 2, 55 + }); 56 + 57 + function getCurrencyFormatter(code?: string) { 58 + const currency = code && code.length === 3 ? code : 'USD'; 59 + try { 60 + return new Intl.NumberFormat('en-US', { 61 + style: 'currency', 62 + currency, 63 + maximumFractionDigits: currency === 'JPY' ? 0 : 2, 64 + }); 65 + } catch { 66 + return new Intl.NumberFormat('en-US', { 67 + style: 'currency', 68 + currency: 'USD', 69 + maximumFractionDigits: 2, 70 + }); 71 + } 72 + } 73 + 74 + function formatCurrency(value?: number, currency?: string) { 75 + if (typeof value !== 'number' || Number.isNaN(value)) { 76 + return '—'; 77 + } 78 + return getCurrencyFormatter(currency).format(value); 79 + } 80 + 81 + function formatNumber(value?: number) { 82 + if (typeof value !== 'number' || Number.isNaN(value)) { 83 + return '—'; 84 + } 85 + return compactNumber.format(value); 86 + } 87 + 88 + function truncateDescription(description?: string) { 89 + if (!description) return undefined; 90 + const clean = sanitizeInput(description); 91 + if (clean.length <= MAX_DESCRIPTION_LENGTH) { 92 + return clean; 93 + } 94 + return `${clean.slice(0, MAX_DESCRIPTION_LENGTH)}…`; 95 + } 96 + 97 + function resolveCurrencyCode(value?: string) { 98 + if (!value) return 'USD'; 99 + const normalized = value.trim().toUpperCase(); 100 + if (normalized.length === 3) { 101 + return normalized; 102 + } 103 + return 'USD'; 104 + } 105 + 106 + function toValidDate(value?: number | string | null) { 107 + if (value === null || value === undefined) { 108 + return undefined; 109 + } 110 + 111 + if (typeof value === 'number' && Number.isFinite(value)) { 112 + const date = new Date(value); 113 + return Number.isNaN(date.getTime()) ? undefined : date; 114 + } 115 + 116 + if (typeof value === 'string' && value.trim().length > 0) { 117 + const date = new Date(value); 118 + return Number.isNaN(date.getTime()) ? undefined : date; 119 + } 120 + 121 + return undefined; 122 + } 123 + 124 + interface StocksRenderOptions { 125 + client: BotClient; 126 + locale: string; 127 + ticker: string; 128 + timeframe: StockTimeframe; 129 + userId: string; 130 + } 131 + 132 + async function buildTimeframeButtons( 133 + client: BotClient, 134 + locale: string, 135 + active: StockTimeframe, 136 + userId: string, 137 + ticker: string, 138 + ) { 139 + const row = new ActionRowBuilder<MessageActionRowComponentBuilder>(); 140 + 141 + for (const timeframe of SUPPORTED_TIMEFRAMES) { 142 + const label = await client.getLocaleText(TIMEFRAME_LABEL_KEYS[timeframe], locale); 143 + row.addComponents( 144 + new ButtonBuilder() 145 + .setCustomId(`${BUTTON_PREFIX}:${userId}:${ticker}:${timeframe}`) 146 + .setLabel(label.toUpperCase()) 147 + .setStyle(timeframe === active ? ButtonStyle.Primary : ButtonStyle.Secondary), 148 + ); 149 + } 150 + 151 + return row; 152 + } 153 + 154 + export async function renderStocksView(options: StocksRenderOptions) { 155 + const normalizedTicker = sanitizeTickerInput(options.ticker); 156 + if (!normalizedTicker) { 157 + const error = new Error('STOCKS_TICKER_NOT_FOUND'); 158 + throw error; 159 + } 160 + 161 + const overview = await getTickerOverview(normalizedTicker); 162 + if (!overview.detail) { 163 + const error = new Error('STOCKS_TICKER_NOT_FOUND'); 164 + throw error; 165 + } 166 + 167 + const aggregates = await getAggregateSeries(normalizedTicker, options.timeframe); 168 + 169 + const detail = overview.detail; 170 + const snapshot = overview.snapshot; 171 + const lastPrice = snapshot?.lastTrade?.p ?? snapshot?.day?.c ?? snapshot?.prevDay?.c; 172 + const prevClose = snapshot?.prevDay?.c; 173 + const changeValue = 174 + snapshot?.todaysChange ?? (lastPrice && prevClose ? lastPrice - prevClose : undefined); 175 + const changePercent = 176 + snapshot?.todaysChangePerc ?? 177 + (changeValue && prevClose ? (changeValue / prevClose) * 100 : undefined); 178 + const trend = 179 + typeof changeValue === 'number' 180 + ? changeValue === 0 181 + ? 'neutral' 182 + : changeValue > 0 183 + ? 'up' 184 + : 'down' 185 + : 'neutral'; 186 + const color = trend === 'up' ? 0x1ac486 : trend === 'down' ? 0xff6b6b : 0x5865f2; 187 + const chartBuffer = aggregates.length 188 + ? await renderStockCandles(aggregates, options.timeframe) 189 + : undefined; 190 + 191 + const [ 192 + priceLabel, 193 + changeLabel, 194 + rangeLabel, 195 + volumeLabel, 196 + prevCloseLabel, 197 + marketCapLabel, 198 + providedBy, 199 + ] = await Promise.all([ 200 + options.client.getLocaleText('commands.stocks.labels.price', options.locale), 201 + options.client.getLocaleText('commands.stocks.labels.change', options.locale), 202 + options.client.getLocaleText('commands.stocks.labels.dayrange', options.locale), 203 + options.client.getLocaleText('commands.stocks.labels.volume', options.locale), 204 + options.client.getLocaleText('commands.stocks.labels.prevclose', options.locale), 205 + options.client.getLocaleText('commands.stocks.labels.marketcap', options.locale), 206 + options.client.getLocaleText('providedby', options.locale), 207 + ]); 208 + 209 + const currencySymbol = resolveCurrencyCode(detail.currency_name); 210 + const description = truncateDescription(detail.description); 211 + const dayLow = snapshot?.day?.l ?? snapshot?.prevDay?.l; 212 + const dayHigh = snapshot?.day?.h ?? snapshot?.prevDay?.h; 213 + const thumbnail = buildBrandingUrl(detail.branding?.icon_url ?? detail.branding?.logo_url); 214 + const footerText = `${providedBy} Massive.com`; 215 + 216 + const embed = new EmbedBuilder() 217 + .setColor(color) 218 + .setTitle(`${normalizedTicker} • ${detail.name}`) 219 + .setFooter({ text: footerText }); 220 + 221 + const timestampDate = toValidDate(snapshot?.updated); 222 + embed.setTimestamp(timestampDate ?? new Date()); 223 + 224 + if (description) { 225 + embed.setDescription(description); 226 + } 227 + 228 + if (thumbnail) { 229 + embed.setThumbnail(thumbnail); 230 + } 231 + 232 + let files: AttachmentBuilder[] = []; 233 + if (chartBuffer) { 234 + const attachmentName = `stocks-${normalizedTicker}-${options.timeframe}.png`; 235 + const attachment = new AttachmentBuilder(chartBuffer, { name: attachmentName }); 236 + embed.setImage(`attachment://${attachmentName}`); 237 + files = [attachment]; 238 + } else { 239 + embed.addFields({ 240 + name: '\u200B', 241 + value: await options.client.getLocaleText('commands.stocks.labels.nochart', options.locale), 242 + }); 243 + } 244 + 245 + embed.addFields( 246 + { 247 + name: priceLabel, 248 + value: formatCurrency(lastPrice, currencySymbol), 249 + inline: true, 250 + }, 251 + { 252 + name: changeLabel, 253 + value: 254 + typeof changeValue === 'number' 255 + ? changePercent 256 + ? `${formatCurrency(changeValue, currencySymbol)} (${changePercent.toFixed(2)}%)` 257 + : formatCurrency(changeValue, currencySymbol) 258 + : '—', 259 + inline: true, 260 + }, 261 + { 262 + name: rangeLabel, 263 + value: `${formatCurrency(dayLow, currencySymbol)} - ${formatCurrency(dayHigh, currencySymbol)}`, 264 + inline: true, 265 + }, 266 + { 267 + name: volumeLabel, 268 + value: formatNumber(snapshot?.day?.v ?? snapshot?.prevDay?.v), 269 + inline: true, 270 + }, 271 + { 272 + name: prevCloseLabel, 273 + value: formatCurrency(prevClose, currencySymbol), 274 + inline: true, 275 + }, 276 + { 277 + name: marketCapLabel, 278 + value: formatNumber(detail.market_cap), 279 + inline: true, 280 + }, 281 + ); 282 + 283 + const buttons = await buildTimeframeButtons( 284 + options.client, 285 + options.locale, 286 + options.timeframe, 287 + options.userId, 288 + normalizedTicker, 289 + ); 290 + 291 + return { 292 + embeds: [embed], 293 + components: [buttons], 294 + files, 295 + }; 296 + } 297 + 298 + export default { 299 + data: new SlashCommandBuilder() 300 + .setName('stocks') 301 + .setDescription('Track stock prices and view quick charts') 302 + .addStringOption((option) => 303 + option 304 + .setName('ticker') 305 + .setDescription('The stock ticker symbol (e.g., AAPL, TSLA)') 306 + .setRequired(true) 307 + .setMaxLength(15), 308 + ) 309 + .addStringOption((option) => 310 + option 311 + .setName('range') 312 + .setDescription('Initial timeframe for the chart') 313 + .addChoices( 314 + { name: '1D', value: '1d' }, 315 + { name: '5D', value: '5d' }, 316 + { name: '1M', value: '1m' }, 317 + { name: '3M', value: '3m' }, 318 + { name: '1Y', value: '1y' }, 319 + ), 320 + ) 321 + .setContexts([ 322 + InteractionContextType.BotDM, 323 + InteractionContextType.Guild, 324 + InteractionContextType.PrivateChannel, 325 + ]) 326 + .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 327 + 328 + async execute(client, interaction) { 329 + try { 330 + const cooldownCheck = await checkCooldown( 331 + cooldownManager, 332 + interaction.user.id, 333 + client, 334 + interaction.locale, 335 + ); 336 + if (cooldownCheck.onCooldown) { 337 + return interaction.reply(createCooldownResponse(cooldownCheck.message!)); 338 + } 339 + 340 + if (!config.MASSIVE_API_KEY) { 341 + const msg = await client.getLocaleText( 342 + 'commands.stocks.errors.noapikey', 343 + interaction.locale, 344 + ); 345 + return interaction.reply({ content: msg, flags: MessageFlags.Ephemeral }); 346 + } 347 + 348 + setCooldown(cooldownManager, interaction.user.id); 349 + 350 + const tickerInput = interaction.options.getString('ticker', true); 351 + const timeframeInput = 352 + (interaction.options.getString('range') as StockTimeframe | null) ?? DEFAULT_TIMEFRAME; 353 + const ticker = sanitizeTickerInput(tickerInput); 354 + 355 + if (!ticker) { 356 + const notFound = await client.getLocaleText( 357 + 'commands.stocks.errors.notfound', 358 + interaction.locale, 359 + { 360 + ticker: tickerInput, 361 + }, 362 + ); 363 + return interaction.reply({ content: notFound, flags: MessageFlags.Ephemeral }); 364 + } 365 + 366 + commandLogger.logFromInteraction( 367 + interaction, 368 + `ticker: ${ticker} timeframe: ${timeframeInput}`, 369 + ); 370 + 371 + await interaction.deferReply(); 372 + 373 + try { 374 + const response = await renderStocksView({ 375 + client, 376 + locale: interaction.locale, 377 + ticker, 378 + timeframe: timeframeInput, 379 + userId: interaction.user.id, 380 + }); 381 + 382 + await interaction.editReply(response); 383 + } catch (error) { 384 + if ((error as Error).message === 'STOCKS_TICKER_NOT_FOUND') { 385 + const notFound = await client.getLocaleText( 386 + 'commands.stocks.errors.notfound', 387 + interaction.locale, 388 + { ticker }, 389 + ); 390 + await interaction.editReply({ content: notFound, components: [] }); 391 + return; 392 + } 393 + 394 + await errorHandler({ 395 + interaction, 396 + client, 397 + error: error as Error, 398 + userId: interaction.user.id, 399 + username: interaction.user.tag, 400 + }); 401 + } 402 + } catch (error) { 403 + logger.error('Unexpected error in stocks command:', error); 404 + if (!interaction.replied && !interaction.deferred) { 405 + await interaction.reply({ 406 + content: await client.getLocaleText('unexpectederror', interaction.locale), 407 + flags: MessageFlags.Ephemeral, 408 + }); 409 + } else if (interaction.deferred) { 410 + const errorMsg = await client.getLocaleText('unexpectederror', interaction.locale); 411 + await interaction.editReply({ content: errorMsg }); 412 + } 413 + } 414 + }, 415 + } as SlashCommandProps; 416 + 417 + export function parseStocksButtonId(customId: string) { 418 + if (!customId.startsWith(`${BUTTON_PREFIX}:`)) return null; 419 + const [, userId, ticker, timeframe] = customId.split(':'); 420 + if (!userId || !ticker || !SUPPORTED_TIMEFRAMES.includes(timeframe as StockTimeframe)) { 421 + return null; 422 + } 423 + return { userId, ticker, timeframe: timeframe as StockTimeframe }; 424 + }
+2
src/config/index.ts
··· 25 25 export const DATABASE_URL = process.env.DATABASE_URL!; 26 26 export const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; 27 27 export const OPENWEATHER_API_KEY = process.env.OPENWEATHER_API_KEY; 28 + export const MASSIVE_API_KEY = process.env.MASSIVE_API_KEY; 29 + export const MASSIVE_API_BASE_URL = process.env.MASSIVE_API_BASE_URL ?? 'https://api.massive.com'; 28 30 export const SOURCE_COMMIT = process.env.SOURCE_COMMIT; 29 31 export const TOKEN = process.env.TOKEN!; 30 32 export const CLIENT_ID = process.env.CLIENT_ID!;
+47
src/events/interactionCreate.ts
··· 1 1 import { browserHeaders } from '@/constants/index'; 2 2 import BotClient from '@/services/Client'; 3 + import * as config from '@/config'; 4 + import { renderStocksView, parseStocksButtonId } from '@/commands/utilities/stocks'; 3 5 import { RandomReddit } from '@/types/base'; 4 6 import { RemindCommandProps } from '@/types/command'; 5 7 import logger from '@/utils/logger'; ··· 127 129 } 128 130 ).handleButton(this.client, i); 129 131 } 132 + } 133 + 134 + const stocksPayload = parseStocksButtonId(i.customId); 135 + if (stocksPayload) { 136 + if (!config.MASSIVE_API_KEY) { 137 + const message = await this.client.getLocaleText( 138 + 'commands.stocks.errors.noapikey', 139 + i.locale, 140 + ); 141 + return await i.reply({ content: message, flags: MessageFlags.Ephemeral }); 142 + } 143 + 144 + if (stocksPayload.userId !== i.user.id) { 145 + const unauthorized = 146 + (await this.client.getLocaleText('commands.stocks.errors.unauthorized', i.locale)) || 147 + 'Only the person who used /stocks can use these buttons.'; 148 + return await i.reply({ content: unauthorized, flags: MessageFlags.Ephemeral }); 149 + } 150 + 151 + await i.deferUpdate(); 152 + 153 + try { 154 + const response = await renderStocksView({ 155 + client: this.client, 156 + locale: i.locale, 157 + ticker: stocksPayload.ticker, 158 + timeframe: stocksPayload.timeframe, 159 + userId: stocksPayload.userId, 160 + }); 161 + await i.editReply(response); 162 + } catch (error) { 163 + if ((error as Error).message === 'STOCKS_TICKER_NOT_FOUND') { 164 + const notFound = await this.client.getLocaleText( 165 + 'commands.stocks.errors.notfound', 166 + i.locale, 167 + { ticker: stocksPayload.ticker }, 168 + ); 169 + await i.editReply({ content: notFound, components: [] }); 170 + } else { 171 + logger.error('Error updating stocks view:', error); 172 + const failMsg = await this.client.getLocaleText('failedrequest', i.locale); 173 + await i.editReply({ content: failMsg, components: [] }); 174 + } 175 + } 176 + return; 130 177 } 131 178 132 179 const originalUser = i.message.interaction!.user;
+280
src/services/massive.ts
··· 1 + import { 2 + DefaultApi, 3 + GetStocksAggregatesSortEnum, 4 + GetStocksAggregatesTimespanEnum, 5 + GetStocksSnapshotTicker200Response, 6 + GetStocksSnapshotTicker200ResponseAllOfTicker, 7 + GetTicker200Response, 8 + GetTicker200ResponseResults, 9 + GetStocksAggregates200Response, 10 + ListTickers200Response, 11 + ListTickers200ResponseResultsInner, 12 + ListTickersMarketEnum, 13 + ListTickersOrderEnum, 14 + ListTickersSortEnum, 15 + restClient, 16 + } from '@massive.com/client-js'; 17 + import * as config from '@/config'; 18 + import logger from '@/utils/logger'; 19 + import { createRateLimiter } from '@/utils/rateLimiter'; 20 + import type { AxiosError } from 'axios'; 21 + 22 + const MASSIVE_RATE_LIMIT = 45; 23 + const rateLimiter = createRateLimiter(MASSIVE_RATE_LIMIT); 24 + 25 + let cachedClient: DefaultApi | null = null; 26 + 27 + export type StockTimeframe = '1d' | '5d' | '1m' | '3m' | '1y'; 28 + 29 + interface TimeframeConfig { 30 + multiplier: number; 31 + timespan: GetStocksAggregatesTimespanEnum; 32 + daysBack: number; 33 + limit: number; 34 + displayWindowMs?: number; 35 + } 36 + 37 + const TIMEFRAME_CONFIG: Record<StockTimeframe, TimeframeConfig> = { 38 + '1d': { 39 + multiplier: 5, 40 + timespan: GetStocksAggregatesTimespanEnum.Minute, 41 + daysBack: 3, 42 + limit: 400, 43 + displayWindowMs: 36 * 60 * 60 * 1000, 44 + }, 45 + '5d': { 46 + multiplier: 15, 47 + timespan: GetStocksAggregatesTimespanEnum.Minute, 48 + daysBack: 7, 49 + limit: 500, 50 + displayWindowMs: 7 * 24 * 60 * 60 * 1000, 51 + }, 52 + '1m': { 53 + multiplier: 1, 54 + timespan: GetStocksAggregatesTimespanEnum.Day, 55 + daysBack: 40, 56 + limit: 120, 57 + }, 58 + '3m': { 59 + multiplier: 1, 60 + timespan: GetStocksAggregatesTimespanEnum.Day, 61 + daysBack: 110, 62 + limit: 200, 63 + }, 64 + '1y': { 65 + multiplier: 1, 66 + timespan: GetStocksAggregatesTimespanEnum.Week, 67 + daysBack: 400, 68 + limit: 400, 69 + }, 70 + }; 71 + 72 + const DAY_MS = 24 * 60 * 60 * 1000; 73 + 74 + export interface StockAggregatePoint { 75 + timestamp: number; 76 + open: number; 77 + high: number; 78 + low: number; 79 + close: number; 80 + volume: number; 81 + vwap?: number; 82 + } 83 + 84 + export interface StockOverview { 85 + detail?: GetTicker200ResponseResults; 86 + snapshot?: GetStocksSnapshotTicker200ResponseAllOfTicker; 87 + } 88 + 89 + function ensureClient(): DefaultApi { 90 + if (!config.MASSIVE_API_KEY) { 91 + throw new Error('Massive.com API key is not configured'); 92 + } 93 + 94 + if (!cachedClient) { 95 + cachedClient = restClient(config.MASSIVE_API_KEY, config.MASSIVE_API_BASE_URL, { 96 + pagination: false, 97 + }); 98 + } 99 + 100 + return cachedClient; 101 + } 102 + 103 + async function withClient<T>(callback: (client: DefaultApi) => Promise<T>): Promise<T> { 104 + const client = ensureClient(); 105 + return rateLimiter.schedule(() => callback(client)); 106 + } 107 + 108 + function isNotFoundError(error: unknown): boolean { 109 + return ( 110 + typeof error === 'object' && 111 + error !== null && 112 + 'isAxiosError' in error && 113 + (error as AxiosError).response?.status === 404 114 + ); 115 + } 116 + 117 + export async function searchTickers( 118 + query: string, 119 + limit = 5, 120 + ): Promise<ListTickers200ResponseResultsInner[]> { 121 + if (!query.trim()) { 122 + return []; 123 + } 124 + 125 + const response = await withClient((client) => 126 + client.listTickers( 127 + undefined, 128 + undefined, 129 + ListTickersMarketEnum.Stocks, 130 + undefined, 131 + undefined, 132 + undefined, 133 + undefined, 134 + query, 135 + true, 136 + undefined, 137 + undefined, 138 + undefined, 139 + undefined, 140 + ListTickersOrderEnum.Asc, 141 + limit, 142 + ListTickersSortEnum.Ticker, 143 + ), 144 + ); 145 + 146 + return response.results ?? []; 147 + } 148 + 149 + export async function getTickerDetails( 150 + ticker: string, 151 + ): Promise<GetTicker200ResponseResults | null> { 152 + const normalized = ticker.trim().toUpperCase(); 153 + try { 154 + const response = await withClient((client) => client.getTicker(normalized)); 155 + return response.results ?? null; 156 + } catch (error) { 157 + if (isNotFoundError(error)) { 158 + return null; 159 + } 160 + throw error; 161 + } 162 + } 163 + 164 + export async function getTickerSnapshot( 165 + ticker: string, 166 + ): Promise<GetStocksSnapshotTicker200ResponseAllOfTicker | undefined> { 167 + const normalized = ticker.trim().toUpperCase(); 168 + try { 169 + const response: GetStocksSnapshotTicker200Response = await withClient((client) => 170 + client.getStocksSnapshotTicker(normalized), 171 + ); 172 + return response.ticker; 173 + } catch (error) { 174 + if (isNotFoundError(error)) { 175 + return undefined; 176 + } 177 + throw error; 178 + } 179 + } 180 + 181 + export async function getTickerOverview(ticker: string): Promise<StockOverview> { 182 + const [detail, snapshot] = await Promise.all([ 183 + getTickerDetails(ticker), 184 + getTickerSnapshot(ticker), 185 + ]); 186 + 187 + return { detail: detail ?? undefined, snapshot }; 188 + } 189 + 190 + function formatDate(date: Date): string { 191 + return date.toISOString().split('T')[0]; 192 + } 193 + 194 + export async function getAggregateSeries( 195 + ticker: string, 196 + timeframe: StockTimeframe, 197 + ): Promise<StockAggregatePoint[]> { 198 + const config = TIMEFRAME_CONFIG[timeframe]; 199 + if (!config) { 200 + throw new Error(`Unsupported timeframe: ${timeframe}`); 201 + } 202 + 203 + const now = new Date(); 204 + const fetchAggregates = async (extraDays: number) => { 205 + const fromDate = new Date(now.getTime() - (config.daysBack + extraDays) * DAY_MS); 206 + return withClient((client) => 207 + client.getStocksAggregates( 208 + ticker.trim().toUpperCase(), 209 + config.multiplier, 210 + config.timespan, 211 + formatDate(fromDate), 212 + formatDate(now), 213 + true, 214 + GetStocksAggregatesSortEnum.Asc, 215 + config.limit, 216 + ), 217 + ); 218 + }; 219 + 220 + try { 221 + let response: GetStocksAggregates200Response = await fetchAggregates(0); 222 + 223 + if ((!response.results || response.results.length === 0) && timeframe === '1d') { 224 + response = await fetchAggregates(5); 225 + } 226 + 227 + const rawResults = response.results ?? []; 228 + if (!rawResults.length) { 229 + return []; 230 + } 231 + 232 + let filteredResults = rawResults.filter( 233 + (result) => typeof result.t === 'number' && typeof result.c === 'number', 234 + ); 235 + 236 + if (config.displayWindowMs && filteredResults.length) { 237 + const latestTimestamp = filteredResults[filteredResults.length - 1].t!; 238 + const cutoff = latestTimestamp - config.displayWindowMs; 239 + filteredResults = filteredResults.filter((result) => result.t! >= cutoff); 240 + } 241 + 242 + return filteredResults.map((result) => ({ 243 + timestamp: result.t!, 244 + open: result.o ?? result.c ?? 0, 245 + high: result.h ?? result.c ?? 0, 246 + low: result.l ?? result.c ?? 0, 247 + close: result.c ?? 0, 248 + volume: result.v ?? 0, 249 + vwap: result.vw, 250 + })); 251 + } catch (error) { 252 + if (isNotFoundError(error)) { 253 + throw new Error('STOCKS_TICKER_NOT_FOUND'); 254 + } 255 + throw error; 256 + } 257 + } 258 + 259 + export function buildBrandingUrl(url?: string): string | undefined { 260 + if (!url) return undefined; 261 + 262 + try { 263 + const parsed = new URL(url); 264 + if (!parsed.searchParams.has('apiKey') && config.MASSIVE_API_KEY) { 265 + parsed.searchParams.set('apiKey', config.MASSIVE_API_KEY); 266 + } 267 + return parsed.toString(); 268 + } catch (error) { 269 + logger.warn('Failed to parse branding URL', { url, error }); 270 + return undefined; 271 + } 272 + } 273 + 274 + export function sanitizeTickerInput(input: string): string { 275 + return input 276 + .trim() 277 + .toUpperCase() 278 + .replace(/[^A-Z0-9.\-]/g, '') 279 + .slice(0, 12); 280 + }
+31
src/utils/rateLimiter.ts
··· 1 + type RateLimitedTask<T> = () => Promise<T> | T; 2 + 3 + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 4 + 5 + export function createRateLimiter(limitPerSecond: number) { 6 + const minDelay = Math.ceil(1000 / Math.max(1, limitPerSecond)); 7 + let lastRun = 0; 8 + let chain: Promise<void> = Promise.resolve(); 9 + 10 + const schedule = async <T>(task: RateLimitedTask<T>): Promise<T> => { 11 + const execute = chain.then(async () => { 12 + const elapsed = Date.now() - lastRun; 13 + const waitTime = lastRun === 0 ? 0 : Math.max(0, minDelay - elapsed); 14 + if (waitTime > 0) { 15 + await sleep(waitTime); 16 + } 17 + 18 + const result = await task(); 19 + lastRun = Date.now(); 20 + return result; 21 + }); 22 + 23 + chain = execute.then( 24 + () => undefined, 25 + () => undefined, 26 + ); 27 + return execute; 28 + }; 29 + 30 + return { schedule }; 31 + }
+199
src/utils/stockChart.ts
··· 1 + import { createCanvas } from 'canvas'; 2 + import { StockAggregatePoint, StockTimeframe } from '@/services/massive'; 3 + 4 + const WIDTH = 900; 5 + const HEIGHT = 460; 6 + const PADDING = { 7 + top: 24, 8 + right: 32, 9 + bottom: 48, 10 + left: 64, 11 + }; 12 + const MIN_SPACING = 6; 13 + const UP_COLOR = '#1AC486'; 14 + const DOWN_COLOR = '#FF6B6B'; 15 + const GRID_COLOR = 'rgba(255,255,255,0.08)'; 16 + const AXIS_COLOR = 'rgba(255,255,255,0.4)'; 17 + const TEXT_COLOR = 'rgba(255,255,255,0.85)'; 18 + const BACKGROUND = '#0f1117'; 19 + 20 + const TIME_LABEL_FORMATTER = new Intl.DateTimeFormat('en-US', { 21 + hour: 'numeric', 22 + minute: '2-digit', 23 + }); 24 + const DATE_LABEL_FORMATTER = new Intl.DateTimeFormat('en-US', { 25 + month: 'short', 26 + day: 'numeric', 27 + }); 28 + const WEEKDAY_LABEL_FORMATTER = new Intl.DateTimeFormat('en-US', { 29 + weekday: 'short', 30 + month: 'short', 31 + day: 'numeric', 32 + }); 33 + 34 + function formatLabel(timestamp: number, timeframe?: StockTimeframe) { 35 + const date = new Date(timestamp); 36 + if (Number.isNaN(date.getTime())) return ''; 37 + if (timeframe === '1d') return TIME_LABEL_FORMATTER.format(date); 38 + if (timeframe === '5d') return WEEKDAY_LABEL_FORMATTER.format(date); 39 + return DATE_LABEL_FORMATTER.format(date); 40 + } 41 + 42 + export async function renderStockCandles( 43 + points: StockAggregatePoint[], 44 + timeframe?: StockTimeframe, 45 + ): Promise<Buffer> { 46 + if (!points.length) { 47 + throw new Error('No aggregate data available for chart'); 48 + } 49 + 50 + const maxCandlesMap: Record<StockTimeframe, number> = { 51 + '1d': 80, 52 + '5d': 110, 53 + '1m': 140, 54 + '3m': 160, 55 + '1y': 160, 56 + }; 57 + const fallbackLimit = 140; 58 + const limit = maxCandlesMap[timeframe ?? '1m'] ?? fallbackLimit; 59 + 60 + const sorted = points.slice(-limit).sort((a, b) => a.timestamp - b.timestamp); 61 + 62 + const chartWidth = WIDTH - PADDING.left - PADDING.right; 63 + const maxVisible = Math.max(3, Math.floor(chartWidth / MIN_SPACING)); 64 + const candles = sorted.slice(-maxVisible).map((point) => ({ 65 + x: point.timestamp, 66 + open: point.open, 67 + high: point.high, 68 + low: point.low, 69 + close: point.close, 70 + })); 71 + 72 + if (!candles.length) { 73 + throw new Error('No aggregate data available for chart'); 74 + } 75 + 76 + const canvas = createCanvas(WIDTH, HEIGHT); 77 + const ctx = canvas.getContext('2d'); 78 + ctx.antialias = 'subpixel'; 79 + 80 + ctx.fillStyle = BACKGROUND; 81 + ctx.fillRect(0, 0, WIDTH, HEIGHT); 82 + 83 + const values = candles.flatMap((candle) => [candle.high, candle.low]); 84 + const rawMax = Math.max(...values); 85 + const rawMin = Math.min(...values); 86 + 87 + const { niceMin, niceMax, tickSpacing } = computeNiceScale(rawMin, rawMax, 5); 88 + const maxPrice = niceMax; 89 + const minPrice = niceMin; 90 + const priceRange = maxPrice - minPrice || 1; 91 + 92 + const chartHeight = HEIGHT - PADDING.top - PADDING.bottom; 93 + 94 + const stepX = candles.length > 1 ? chartWidth / (candles.length - 1) : 0; 95 + const bodyWidth = 96 + candles.length > 1 ? Math.max(4, Math.min(18, stepX * 0.55)) : Math.min(24, chartWidth * 0.2); 97 + 98 + const mapY = (value: number) => 99 + PADDING.top + chartHeight - ((value - minPrice) / priceRange) * chartHeight; 100 + 101 + ctx.strokeStyle = GRID_COLOR; 102 + ctx.lineWidth = 1; 103 + ctx.font = '12px "SF Pro Display", "Segoe UI", sans-serif'; 104 + ctx.fillStyle = TEXT_COLOR; 105 + 106 + const gridLines = Math.max(2, Math.round(priceRange / tickSpacing)); 107 + for (let i = 0; i <= gridLines; i++) { 108 + const value = maxPrice - tickSpacing * i; 109 + const clampedValue = Math.max(minPrice, Math.min(maxPrice, value)); 110 + const relative = (maxPrice - clampedValue) / priceRange; 111 + const y = PADDING.top + chartHeight * relative; 112 + ctx.beginPath(); 113 + ctx.moveTo(PADDING.left, y); 114 + ctx.lineTo(WIDTH - PADDING.right, y); 115 + ctx.stroke(); 116 + 117 + const priceLabel = clampedValue.toFixed(priceRange >= 10 ? 2 : 3); 118 + ctx.fillText(priceLabel, 16, y + 4); 119 + } 120 + 121 + ctx.strokeStyle = AXIS_COLOR; 122 + ctx.beginPath(); 123 + ctx.moveTo(PADDING.left, PADDING.top); 124 + ctx.lineTo(PADDING.left, HEIGHT - PADDING.bottom); 125 + ctx.lineTo(WIDTH - PADDING.right, HEIGHT - PADDING.bottom); 126 + ctx.stroke(); 127 + 128 + candles.forEach((candle, index) => { 129 + const x = candles.length === 1 ? PADDING.left + chartWidth / 2 : PADDING.left + stepX * index; 130 + const openY = mapY(candle.open); 131 + const closeY = mapY(candle.close); 132 + const highY = mapY(candle.high); 133 + const lowY = mapY(candle.low); 134 + const color = candle.close >= candle.open ? UP_COLOR : DOWN_COLOR; 135 + 136 + ctx.strokeStyle = color; 137 + ctx.lineWidth = 1.5; 138 + ctx.beginPath(); 139 + ctx.moveTo(x, highY); 140 + ctx.lineTo(x, lowY); 141 + ctx.stroke(); 142 + 143 + ctx.beginPath(); 144 + const bodyHeight = Math.max(2, Math.abs(closeY - openY)); 145 + const bodyTop = Math.min(openY, closeY); 146 + ctx.rect(x - bodyWidth / 2, bodyTop, bodyWidth, bodyHeight || 2); 147 + ctx.fillStyle = color; 148 + ctx.fill(); 149 + }); 150 + 151 + const labelCount = Math.min(6, candles.length); 152 + for (let i = 0; i < labelCount; i++) { 153 + const candleIndex = Math.round((i / Math.max(1, labelCount - 1)) * (candles.length - 1)); 154 + const label = formatLabel(candles[candleIndex].x, timeframe); 155 + const x = 156 + candles.length === 1 ? PADDING.left + chartWidth / 2 : PADDING.left + stepX * candleIndex; 157 + ctx.fillStyle = TEXT_COLOR; 158 + ctx.fillText(label, x - ctx.measureText(label).width / 2, HEIGHT - PADDING.bottom + 20); 159 + } 160 + 161 + return canvas.toBuffer('image/png'); 162 + } 163 + 164 + function computeNiceScale(min: number, max: number, maxTicks: number) { 165 + if (!Number.isFinite(min) || !Number.isFinite(max)) { 166 + return { niceMin: 0, niceMax: 1, tickSpacing: 0.2 }; 167 + } 168 + if (min === max) { 169 + const offset = Math.abs(min) * 0.05 || 1; 170 + min -= offset; 171 + max += offset; 172 + } 173 + 174 + const range = niceNum(max - min, false); 175 + const tickSpacing = niceNum(range / (maxTicks - 1), true); 176 + const niceMin = Math.floor(min / tickSpacing) * tickSpacing; 177 + const niceMax = Math.ceil(max / tickSpacing) * tickSpacing; 178 + 179 + return { niceMin, niceMax, tickSpacing }; 180 + } 181 + 182 + function niceNum(range: number, round: boolean) { 183 + const exponent = Math.floor(Math.log10(range)); 184 + const fraction = range / Math.pow(10, exponent); 185 + let niceFraction; 186 + 187 + if (round) { 188 + if (fraction < 1.5) niceFraction = 1; 189 + else if (fraction < 3) niceFraction = 2; 190 + else if (fraction < 7) niceFraction = 5; 191 + else niceFraction = 10; 192 + } else { 193 + if (fraction <= 1) niceFraction = 1; 194 + else if (fraction <= 2) niceFraction = 2; 195 + else if (fraction <= 5) niceFraction = 5; 196 + else niceFraction = 10; 197 + } 198 + return niceFraction * Math.pow(10, exponent); 199 + }