+5
-1
.env.example
+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
+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
+2
environment.d.ts
+37
locales/en-US.json
+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
+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
+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
+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
+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
+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
+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
+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
+
}