all repos — stealth-developers @ 0ad474abcf4c9b5950a6297e3403fb7537b0186b

ticket system
vi v@vt3e.cat
Wed, 28 Jan 2026 19:05:31 +0000
commit

0ad474abcf4c9b5950a6297e3403fb7537b0186b

parent

7f079d8ff07713cc317b566bceb3fe34b42c4ce0

M .gitignore.gitignore

@@ -1,6 +1,7 @@

# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore .config.*json !.config.example.json +data # Logs
D a

@@ -1,2 +0,0 @@

-a: https://groups.roblox.com/v1/groups/12266404/forums/f7fd4713-6f06-45ad-b3e7-9e57299069c4/posts/9719aaab-1727-4ede-b3d2-04056f5db8d9 -b: https://groups.roblox.com/v1/groups/12266404/forums/f7fd4713-6f06-45ad-b3e7-9e57299069c4/posts/9719aaab-1727-4ede-b3d2-04056f5db8d9
M bun.lockbun.lock

@@ -4,17 +4,17 @@ "workspaces": {

"": { "name": "template", "dependencies": { - "@google-cloud/vision": "^5.3.4", - "@sillowww/lily": "^0.2.0", + "@libsql/client": "^0.17.0", "discord.js": "^14.24.0", - "mongodb": "^6.20.0", - "mongoose": "^8.19.2", - "zod": "^3.25.76", + "drizzle-orm": "^1.0.0-beta.12-a5629fb", + "pino": "^10.3.0", + "pino-pretty": "^13.1.3", + "zod": "^4.3.6", }, "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/bun": "latest", - "@types/mongoose": "^5.11.97", + "@types/bun": "^1.3.7", + "drizzle-kit": "^1.0.0-beta.12-a5629fb", }, "peerDependencies": { "typescript": "^5.8.3",

@@ -22,6 +22,40 @@ },

}, }, "packages": { + "@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], + + "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], + + "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], + + "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], + + "@azure/core-http-compat": ["@azure/core-http-compat@2.3.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g=="], + + "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], + + "@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="], + + "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="], + + "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], + + "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], + + "@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + + "@azure/keyvault-common": ["@azure/keyvault-common@2.0.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w=="], + + "@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="], + + "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], + + "@azure/msal-browser": ["@azure/msal-browser@4.28.1", "", { "dependencies": { "@azure/msal-common": "15.14.1" } }, "sha512-al2u2fTchbClq3L4C1NlqLm+vwKfhYCPtZN2LR/9xJVaQ4Mnrwf5vANvuyPSJHcGvw50UBmhuVmYUAhTEetTpA=="], + + "@azure/msal-common": ["@azure/msal-common@15.14.1", "", {}, "sha512-IkzF7Pywt6QKTS0kwdCv/XV8x8JXknZDvSjj/IccooxnP373T5jaadO3FnOrbWo3S0UqkfIDyZNTaQ/oAgRdXw=="], + + "@azure/msal-node": ["@azure/msal-node@3.8.6", "", { "dependencies": { "@azure/msal-common": "15.14.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],

@@ -52,37 +86,93 @@ "@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],

"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], - "@google-cloud/promisify": ["@google-cloud/promisify@5.0.0", "", {}, "sha512-N8qS6dlORGHwk7WjGXKOSsLjIjNINCPicsOX6gyyLiYk7mq3MtII96NZ9N2ahwA2vnkLmZODOIH9rlNniYWvCQ=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], - "@google-cloud/vision": ["@google-cloud/vision@5.3.4", "", { "dependencies": { "@google-cloud/promisify": "^5.0.0", "google-gax": "^5.0.0" } }, "sha512-KQ/0/klu6jranByRv+BpZbSBFirJNE07AO0wNhUTlYpKRv4UnSvICAVegWXQWN4oljr9EasWyft1667lhFFPAQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.0", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.2", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], + + "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], + + "@libsql/client": ["@libsql/client@0.17.0", "", { "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg=="], + + "@libsql/core": ["@libsql/core@0.17.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw=="], + + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.22", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA=="], + + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA=="], + + "@libsql/hrana-client": ["@libsql/hrana-client@0.9.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "cross-fetch": "^4.0.0", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw=="], + + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], + + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA=="], + + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg=="], + + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA=="], + + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw=="], + + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew=="], + + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg=="], + + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], + + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],

@@ -90,175 +180,225 @@ "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],

"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], - "@sillowww/lily": ["@sillowww/lily@0.2.0", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-ayQi4anINUfu5B/evMIBIMD9BlOueeSKwGXoE+z+VtCVxc2ypMEb3sJyMFqjiStAW7ORMGM3oLV65JFK6PoaUw=="], - - "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], - "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], + "@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="], - "@types/mongoose": ["@types/mongoose@5.11.97", "", { "dependencies": { "mongoose": "*" } }, "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q=="], + "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="], "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], - "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="], - "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], - - "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="], "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="], - "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], - "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=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "discord-api-types": ["discord-api-types@0.38.31", "", {}, "sha512-kC94ANsk8ackj8ENTuO8joTNEL0KtymVhHy9dyEC/s4QAZ7GCx40dYEzQaadyo8w+oP0X8QydE/nzAWRylTGtQ=="], "discord.js": ["discord.js@14.24.0", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.31", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-KNq/ekT8bsmT3ZAfVre8cPbl+DfVYSdlLnDmGZPoz7Cw21LYeWHllRA9MivqNq5b1GPGAxGvyUN1vxbTb/PQWw=="], - "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], + "drizzle-kit": ["drizzle-kit@1.0.0-beta.12-a5629fb", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="], - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "drizzle-orm": ["drizzle-orm@1.0.0-beta.12-a5629fb", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-wyOAgr9Cy9oEN6z5S0JGhfipLKbRRJtQKgbDO9SXGR9swMBbGNIlXkeMqPRrqYQ8k70mh+7ZJ/eVmJ2F7zR3Vg=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - "gaxios": ["gaxios@7.1.2", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "gcp-metadata": ["gcp-metadata@8.1.1", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-dTCcAe9fRQf06ELwel6lWWFrEbstwjUBYEhr5VRGoC+iPDZQucHppCowaIp8b8v92tU1G4X4H3b/Y6zXZxkMsQ=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "google-auth-library": ["google-auth-library@10.4.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-EKiQasw6aEdxSovPEf1oBxCEvxjFamZ6MPaVOSPXZMnqKFLo+rrYjHyjKlFfZcXiKi9qAH6cutr5WRqqa1jKhg=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "google-gax": ["google-gax@5.0.4", "", { "dependencies": { "@grpc/grpc-js": "^1.12.6", "@grpc/proto-loader": "^0.8.0", "duplexify": "^4.1.3", "google-auth-library": "^10.1.0", "google-logging-utils": "^1.1.1", "node-fetch": "^3.3.2", "object-hash": "^3.0.0", "proto3-json-serializer": "^3.0.0", "protobufjs": "^7.5.3", "retry-request": "^8.0.0" } }, "sha512-HmQ6zIYBs2EikTk+kjeHmtHprNTEpsnVaKONw9cwZZwUNCkUb+D5RYrJpCxyjdvIDvJp3wLbVReolJLRZRms1g=="], + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - "google-logging-utils": ["google-logging-utils@1.1.1", "", {}, "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], - "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], - "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - "jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - "kareem": ["kareem@2.6.3", "", {}, "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q=="], + "libsql": ["libsql@0.5.22", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.22", "@libsql/darwin-x64": "0.5.22", "@libsql/linux-arm-gnueabihf": "0.5.22", "@libsql/linux-arm-musleabihf": "0.5.22", "@libsql/linux-arm64-gnu": "0.5.22", "@libsql/linux-arm64-musl": "0.5.22", "@libsql/linux-x64-gnu": "0.5.22", "@libsql/linux-x64-musl": "0.5.22", "@libsql/win32-x64-msvc": "0.5.22" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], - "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], - "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], - "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], - "mongodb": ["mongodb@6.20.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.2" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.3.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ=="], + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], - "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "mongoose": ["mongoose@8.19.2", "", { "dependencies": { "bson": "^6.10.4", "kareem": "2.6.3", "mongodb": "~6.20.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-ww2T4dBV+suCbOfG5YPwj9pLCfUVyj8FEA1D3Ux1HHqutpLxGyOYEPU06iPRBW4cKr3PJfOSYsIpHWPTkz5zig=="], + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], - "mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="], + "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], - "mquery": ["mquery@5.0.0", "", { "dependencies": { "debug": "4.x" } }, "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], + + "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "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=="], - "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "proto3-json-serializer": ["proto3-json-serializer@3.0.3", "", { "dependencies": { "protobufjs": "^7.4.0" } }, "sha512-iUi7jGLuECChuoUwtvf6eXBDcFXTHAt5GM6ckvtD3RqD+j2wW0GW6WndPOu9IWeUk7n933lzrskcNMHJy2tFSw=="], + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "pino": ["pino@10.3.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "pino-pretty": ["pino-pretty@13.1.3", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], - "retry-request": ["retry-request@8.0.2", "", { "dependencies": { "extend": "^3.0.2", "teeny-request": "^10.0.0" } }, "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "sift": ["sift@17.1.3", "", {}, "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "stream-events": ["stream-events@1.0.5", "", { "dependencies": { "stubs": "^3.0.0" } }, "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg=="], + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], - "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "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=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "stubs": ["stubs@3.0.0", "", {}, "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw=="], + "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], - "teeny-request": ["teeny-request@10.1.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^3.3.2", "stream-events": "^1.0.5" } }, "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw=="], + "tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], - "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],

@@ -266,36 +406,80 @@ "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],

"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], - - "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "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=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "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=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "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=="], + + "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], - "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], - "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], - "http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], - "teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], - "teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], } }
A drizzle.config.ts

@@ -0,0 +1,10 @@

+import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + out: "./drizzle", + schema: "./src/database/schema/index.ts", + dialect: "sqlite", + dbCredentials: { + url: "./data/db.sqlite", + }, +});
A drizzle/20260128153333_fearless_doctor_faustus/migration.sql

@@ -0,0 +1,58 @@

+CREATE TABLE `guild` ( + `guild_id` text PRIMARY KEY NOT NULL, + `suggestion_forum_id` text, + `bug_channel_id` text, + `highlights_channel_id` text, + `commands_channel_id` text, + `ticket_channel_id` text, + `ticket_category_id` text, + `ticket_message` text +); +--> statement-breakpoint +CREATE TABLE `manager` ( + `role_id` text PRIMARY KEY NOT NULL, + `guild_id` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `ticket_attachments` ( + `id` text PRIMARY KEY NOT NULL, + `ticket_id` integer NOT NULL, + `message_id` integer, + `file_name` text NOT NULL, + `content_type` text NOT NULL, + `size` integer NOT NULL, + `bucket` text NOT NULL, + `key` text NOT NULL, + `url` text NOT NULL, + `uploaded_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`ticket_id`) REFERENCES `tickets`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`message_id`) REFERENCES `ticket_messages`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `ticket_messages` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `ticket_id` integer NOT NULL, + `author_id` text NOT NULL, + `author_type` text DEFAULT 'user' NOT NULL, + `content` text NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`ticket_id`) REFERENCES `tickets`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `ticket_msgs_ticket_idx` ON `ticket_messages` (`ticket_id`);--> statement-breakpoint +CREATE TABLE `tickets` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `guild_id` text NOT NULL, + `channel_id` text, + `author_id` text NOT NULL, + `anonymous_id` text NOT NULL, + `type` text NOT NULL, + `status` text DEFAULT 'open' NOT NULL, + `topic` text, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `closed_at` integer +); +--> statement-breakpoint +CREATE INDEX `tickets_guild_idx` ON `tickets` (`guild_id`);--> statement-breakpoint +CREATE INDEX `tickets_author_idx` ON `tickets` (`author_id`);--> statement-breakpoint +CREATE INDEX `tickets_status_idx` ON `tickets` (`status`);
A drizzle/20260128153333_fearless_doctor_faustus/snapshot.json

@@ -0,0 +1,537 @@

+{ + "dialect": "sqlite", + "id": "09c91ede-1e86-47c0-a78b-57795ffc3568", + "prevIds": [ + "00000000-0000-0000-0000-000000000000" + ], + "version": "7", + "ddl": [ + { + "name": "guild", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "table": "guild", + "entityType": "columns" + }, + { + "columns": [ + "guild_id" + ], + "nameExplicit": false, + "name": "guild_pk", + "table": "guild", + "entityType": "pks" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "suggestion_forum_id", + "table": "guild", + "entityType": "columns" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "bug_channel_id", + "table": "guild", + "entityType": "columns" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "highlights_channel_id", + "table": "guild", + "entityType": "columns" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands_channel_id", + "table": "guild", + "entityType": "columns" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_channel_id", + "table": "guild", + "entityType": "columns" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_category_id", + "table": "guild", + "entityType": "columns" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_message", + "table": "guild", + "entityType": "columns" + }, + { + "name": "manager", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "role_id", + "table": "manager", + "entityType": "columns" + }, + { + "columns": [ + "role_id" + ], + "nameExplicit": false, + "name": "manager_pk", + "table": "manager", + "entityType": "pks" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "table": "manager", + "entityType": "columns" + }, + { + "name": "ticket_attachments", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_attachments_pk", + "table": "ticket_attachments", + "entityType": "pks" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "file_name", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content_type", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "size", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "bucket", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "key", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())", + "generated": null, + "name": "uploaded_at", + "table": "ticket_attachments", + "entityType": "columns" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "ticket_attachments_ticket_id_tickets_id_fk", + "table": "ticket_attachments", + "entityType": "fks" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "ticket_messages", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "ticket_attachments_message_id_ticket_messages_id_fk", + "table": "ticket_attachments", + "entityType": "fks" + }, + { + "name": "ticket_messages", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "table": "ticket_messages", + "entityType": "columns" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_messages_pk", + "table": "ticket_messages", + "entityType": "pks" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "table": "ticket_messages", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "author_id", + "table": "ticket_messages", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'user'", + "generated": null, + "name": "author_type", + "table": "ticket_messages", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "table": "ticket_messages", + "entityType": "columns" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())", + "generated": null, + "name": "created_at", + "table": "ticket_messages", + "entityType": "columns" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_msgs_ticket_idx", + "table": "ticket_messages", + "entityType": "indexes" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "ticket_messages_ticket_id_tickets_id_fk", + "table": "ticket_messages", + "entityType": "fks" + }, + { + "name": "tickets", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "table": "tickets", + "entityType": "columns" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tickets_pk", + "table": "tickets", + "entityType": "pks" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "table": "tickets", + "entityType": "columns" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "channel_id", + "table": "tickets", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "author_id", + "table": "tickets", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "anonymous_id", + "table": "tickets", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "table": "tickets", + "entityType": "columns" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'open'", + "generated": null, + "name": "status", + "table": "tickets", + "entityType": "columns" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "topic", + "table": "tickets", + "entityType": "columns" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())", + "generated": null, + "name": "created_at", + "table": "tickets", + "entityType": "columns" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "closed_at", + "table": "tickets", + "entityType": "columns" + }, + { + "columns": [ + { + "value": "guild_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "tickets_guild_idx", + "table": "tickets", + "entityType": "indexes" + }, + { + "columns": [ + { + "value": "author_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "tickets_author_idx", + "table": "tickets", + "entityType": "indexes" + }, + { + "columns": [ + { + "value": "status", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "tickets_status_idx", + "table": "tickets", + "entityType": "indexes" + } + ], + "renames": [] +}
M package.jsonpackage.json

@@ -17,18 +17,18 @@ "check": "bunx biome check src/"

}, "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/bun": "latest", - "@types/mongoose": "^5.11.97" + "@types/bun": "^1.3.7", + "drizzle-kit": "^1.0.0-beta.12-a5629fb" }, "peerDependencies": { "typescript": "^5.8.3" }, "dependencies": { - "@google-cloud/vision": "^5.3.4", - "@sillowww/lily": "^0.2.0", + "@libsql/client": "^0.17.0", "discord.js": "^14.24.0", - "mongodb": "^6.20.0", - "mongoose": "^8.19.2", - "zod": "^3.25.76" + "drizzle-orm": "^1.0.0-beta.12-a5629fb", + "pino": "^10.3.0", + "pino-pretty": "^13.1.3", + "zod": "^4.3.6" } }
A src/bot.ts

@@ -0,0 +1,24 @@

+import { Client, Events, GatewayIntentBits } from "discord.js"; + +import registerEvents from "./handlers/events.ts"; +import registerInteractions from "./handlers/interactions.ts"; + +import logger from "./utils/logging.ts"; + +const client = new Client({ + intents: [ + GatewayIntentBits.GuildMessages, + GatewayIntentBits.Guilds, + GatewayIntentBits.MessageContent, + ], +}); + +client.on(Events.ClientReady, async (client) => { + logger.info(`logged in as ${client.user?.tag}!`); + + await Promise.all([registerEvents(client), registerInteractions(client)]); + logger.info("events and interactions registered!"); + console.log(); +}); + +export default client;
M src/config.tssrc/config.ts

@@ -1,20 +1,16 @@

import fs from "node:fs"; import { z } from "zod"; -import lily from "@/utils/logging"; -const logger = lily.child("config"); +import { loggers } from "@/utils/logging"; +const logger = loggers.config; const discordSchema = z.object({ token: z.string(), app_id: z.string(), -}); - -const mongodbSchema = z.object({ - uri: z.string(), - database: z.string(), }); const projectSchema = z.record( + z.string(), z.object({ universe: z.string(), name: z.string(),

@@ -25,67 +21,30 @@ .array(

z.object({ code: z.string(), expired: z.boolean().optional(), - expiredAt: z.string().datetime().optional(), - addedAt: z.string().datetime().optional(), + expiredAt: z.string().optional(), + addedAt: z.string().optional(), }), ) .optional(), }), ); -const googleCloudSchema = z.object({ - credentials: z.object({ - clientEmail: z.string(), - privateKey: z.string(), - }), - projectId: z.string(), -}); - -const catChannelSchema = z.object({ - channelId: z.string(), -}); - -const forumChannel = z.object({ - channelId: z.string(), - channelName: z.string(), -}); -const forumWatcher = z.object({ - enabled: z.boolean().default(false), - interval: z - .number() - .int() - .min(1) - .default(60) - .describe("how often to check for new posts in seconds"), - groupId: z.string(), - groupName: z.string().transform((val) => val.replace(/\s+/g, "-")), - /** @deprecated */ - channelId: z.string(), - channels: z.array(forumChannel).min(1), - notificationChannelId: z.string(), -}); - -const robloxSchema = z.object({ - apiKey: z.string(), - cookie: z.string().optional(), - forumWatcher: forumWatcher.optional(), -}); - -const bloxlinkSchema = z.object({ - token: z.string(), +const s3Schema = z.object({ + accessKeyId: z.string(), + secretAccessKey: z.string(), + bucket: z.string(), + endpoint: z.string().optional(), + region: z.string().default("us-east-1"), + publicUrl: z.string().optional(), }); const schema = z.object({ discord: discordSchema, - mongodb: mongodbSchema, + s3: s3Schema, projects: projectSchema, - bloxlink: bloxlinkSchema.optional(), - roblox: robloxSchema.optional(), trelloBoardId: z.string().optional(), developerId: z.string(), terminology: z.string().default("project"), - googleCloud: googleCloudSchema.optional(), - catChannel: catChannelSchema.optional(), }); function validateConfig() {

@@ -98,7 +57,7 @@ const conf = schema.safeParse(runtimeConfig);

if (conf.success) return conf.data; logger.error("invalid environment variables"); - for (const err of conf.error.errors) + for (const err of conf.error.issues) logger.error(` ${err.message}: ${err.path}`); process.exit(1); }
D src/database/connection.ts

@@ -1,23 +0,0 @@

-import mongoose from "mongoose"; -import cfg from "../config.ts"; -import lily from "../utils/logging.ts"; - -const logger = lily.child("database"); - -export async function connectDatabase() { - try { - logger.info("connecting to mongodb..."); - await mongoose.connect(cfg.data.mongodb.uri, { - dbName: cfg.data.mongodb.database, - }); - logger.info("connected to mongodb"); - } catch (error) { - logger.error("failed to connect to mongodb:", error); - process.exit(1); - } -} - -export async function disconnectDatabase() { - await mongoose.disconnect(); - logger.info("disconnected from mongodb"); -}
A src/database/index.ts

@@ -0,0 +1,16 @@

+import { Database } from "bun:sqlite"; +import { join } from "node:path"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; + +import { DB_FILE } from "@/utils/constants"; + +const sqlite = new Database(DB_FILE); +export const db = drizzle({ client: sqlite }); + +export async function migrateDb() { + console.log("checking database migrations..."); + migrate(db, { migrationsFolder: join(process.cwd(), "drizzle") }); +} + +export * from "./schema";
A src/database/schema/guild.ts

@@ -0,0 +1,35 @@

+import { defineRelations } from "drizzle-orm"; +import { sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const manager = sqliteTable("manager", { + roleId: text("role_id").primaryKey(), + guildId: text("guild_id").notNull(), +}); + +export const guild = sqliteTable("guild", { + guildId: text("guild_id").notNull().primaryKey(), + + suggestion_forum_id: text("suggestion_forum_id"), + bug_channel_id: text("bug_channel_id"), + highlights_channel_id: text("highlights_channel_id"), + commands_channel_id: text("commands_channel_id"), + + ticket_channel_id: text("ticket_channel_id"), + ticket_category_id: text("ticket_category_id"), + ticket_message: text("ticket_message"), +}); + +export const GuildRelations = defineRelations({ manager, guild }, (r) => ({ + manager: { + guild: r.one.guild({ + from: r.manager.guildId, + to: r.guild.guildId, + }), + }, + guild: { + managers: r.many.manager(), + }, +})); + +export type Guild = typeof guild.$inferSelect; +export type Manager = typeof manager.$inferSelect;
A src/database/schema/index.ts

@@ -0,0 +1,2 @@

+export * from "./tickets"; +export * from "./guild";
A src/database/schema/tickets.ts

@@ -0,0 +1,119 @@

+import { defineRelations, sql } from "drizzle-orm"; +import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const tickets = sqliteTable( + "tickets", + { + id: integer("id").primaryKey({ autoIncrement: true }), + guildId: text("guild_id").notNull(), + channelId: text("channel_id"), + + authorId: text("author_id").notNull(), + anonymousId: text("anonymous_id").notNull(), + + type: text("type").notNull(), + status: text("status", { enum: ["open", "closed", "archived"] }) + .default("open") + .notNull(), + + topic: text("topic"), + + createdAt: integer("created_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + closedAt: integer("closed_at", { mode: "timestamp" }), + }, + (t) => [ + index("tickets_guild_idx").on(t.guildId), + index("tickets_author_idx").on(t.authorId), + index("tickets_status_idx").on(t.status), + ], +); + +export const ticketMessages = sqliteTable( + "ticket_messages", + { + id: integer("id").primaryKey({ autoIncrement: true }), + ticketId: integer("ticket_id") + .references(() => tickets.id, { onDelete: "cascade" }) + .notNull(), + + authorId: text("author_id").notNull(), + + authorType: text("author_type", { enum: ["user", "staff", "system"] }) + .default("user") + .notNull(), + + content: text("content").notNull(), + + createdAt: integer("created_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), + }, + (t) => [index("ticket_msgs_ticket_idx").on(t.ticketId)], +); + +export const ticketAttachments = sqliteTable("ticket_attachments", { + id: text("id").primaryKey(), + ticketId: integer("ticket_id") + .references(() => tickets.id, { onDelete: "cascade" }) + .notNull(), + messageId: integer("message_id").references(() => ticketMessages.id, { + onDelete: "cascade", + }), + + fileName: text("file_name").notNull(), + contentType: text("content_type").notNull(), + size: integer("size").notNull(), + + bucket: text("bucket").notNull(), + key: text("key").notNull(), + url: text("url").notNull(), + + uploadedAt: integer("uploaded_at", { mode: "timestamp" }) + .default(sql`(unixepoch())`) + .notNull(), +}); + +export const TicketRelations = defineRelations( + { + tickets, + ticketMessages, + ticketAttachments, + }, + (r) => ({ + tickets: { + messages: r.many.ticketMessages({ + from: r.tickets.id, + to: r.ticketMessages.ticketId, + }), + attachments: r.many.ticketAttachments({ + from: r.tickets.id, + to: r.ticketAttachments.ticketId, + }), + }, + ticketMessages: { + ticket: r.one.tickets({ + from: r.ticketMessages.ticketId, + to: r.tickets.id, + }), + attachments: r.many.ticketAttachments({ + from: r.ticketMessages.id, + to: r.ticketAttachments.messageId, + }), + }, + ticketAttachments: { + ticket: r.one.tickets({ + from: r.ticketAttachments.ticketId, + to: r.tickets.id, + }), + message: r.one.ticketMessages({ + from: r.ticketAttachments.messageId, + to: r.ticketMessages.id, + }), + }, + }), +); + +export type Ticket = typeof tickets.$inferSelect; +export type TicketMessage = typeof ticketMessages.$inferSelect;
D src/database/schemas.ts

@@ -1,96 +0,0 @@

-import { - type HydratedDocument, - type InferSchemaType, - Schema, - model, -} from "mongoose"; -import config from "../config"; -const uniqueProjects = Object.keys(config.data.projects) as [ - string, - ...string[], -]; - -const CounterSchema = new Schema({ - _id: String, - sequence_value: { type: Number, default: 0 }, -}); - -const GuildSchema = new Schema( - { - guild_id: { type: String, required: true, unique: true }, - suggestion_forum: String, - bug_channel: String, - commands_channel: String, - highlights_channel: String, - manager_roles: [String], - report_message: { type: String, default: null }, - report_channel: { type: String, default: null }, - }, - { timestamps: true }, -); - -const UserSchema = new Schema( - { - user_id: { type: String, required: true }, - guild_id: { type: String, required: true, ref: "Guild" }, - cat_points: { type: Number, default: 0 }, - /** @deprecated */ - has_reported: { type: Boolean, default: false }, - last_report: { type: Date, default: null }, - }, - { timestamps: true }, -); - -const BugSchema = new Schema( - { - bug_id: { type: Number, required: true, unique: true }, - user_id: { type: String, required: true, ref: "User" }, - status: { type: String, enum: ["open", "closed"], default: "open" }, - title: { type: String, required: true }, - /** @deprecated */ - project: { type: String, enum: uniqueProjects, required: false }, - projects: [{ type: String, enum: uniqueProjects, required: false }], - description: { type: String, required: true }, - sent: { type: Boolean, default: false }, - message_id: String, - thread_id: String, - }, - { timestamps: true }, -); - -const MediaSchema = new Schema( - { - media_type: { type: String, enum: ["image", "video"], required: true }, - data: { type: Buffer, required: true }, - user_id: { type: String, required: true, ref: "User" }, - bug_id: { type: String, ref: "Bug" }, - extension: { type: String, required: true }, - }, - { timestamps: true }, -); - -// compound indexes for better perf -UserSchema.index({ user_id: 1, guild_id: 1 }, { unique: true }); - -// types -export type GuildType = HydratedDocument<InferSchemaType<typeof GuildSchema>>; -export type UserType = HydratedDocument<InferSchemaType<typeof UserSchema>>; -export type BugType = HydratedDocument<InferSchemaType<typeof BugSchema>>; -export type MediaType = HydratedDocument<InferSchemaType<typeof MediaSchema>>; - -// models -export const GuildModel = model<GuildType>("Guild", GuildSchema); -export const UserModel = model<UserType>("User", UserSchema); -export const BugModel = model<BugType>("Bug", BugSchema); -export const MediaModel = model<MediaType>("Media", MediaSchema); -export const CounterModel = model("Counter", CounterSchema); - -// funcs -export async function getNextBugId(): Promise<number> { - const counter = await CounterModel.findByIdAndUpdate( - "bug_id", - { $inc: { sequence_value: 1 } }, - { new: true, upsert: true }, - ); - return counter.sequence_value; -}
M src/events/interactionCreate.tssrc/events/interactionCreate.ts

@@ -15,8 +15,8 @@ try {

await command.execute(client, interaction); } catch (error) { logger.error( - `there was an error while executing ${interaction.commandName}`, error, + `there was an error while executing ${interaction.commandName}`, ); interaction.deferReply({ flags: ["Ephemeral"] }); await interaction.followUp({

@@ -37,8 +37,8 @@ try {

await command.buttonExecute(client, interaction); } catch (error) { logger.error( - `there was an error while executing button with customId ${interaction.customId}`, error, + `there was an error while executing button with customId ${interaction.customId}`, ); await interaction.reply({ content: "there was an error while executing this button command!",

@@ -53,8 +53,8 @@ try {

await command.modalExecute(client, interaction); } catch (error) { logger.error( - `there was an error while executing modal with customId ${interaction.customId}`, error, + `there was an error while executing modal with customId ${interaction.customId}`, ); await interaction.reply({ content: "there was an error while executing this modal!",

@@ -68,8 +68,8 @@ try {

await command.autocomplete(interaction); } catch (error) { logger.error( - `there was an error while executing autocomplete command for ${interaction.commandName}`, error, + `there was an error while executing autocomplete command for ${interaction.commandName}`, ); } } else if (interaction.isStringSelectMenu()) {

@@ -83,8 +83,8 @@ try {

await command.selectMenuExecute(client, interaction); } catch (error) { logger.error( - `there was an error while executing select menu with customId ${interaction.customId}`, error, + `there was an error while executing select menu with customId ${interaction.customId}`, ); await interaction.reply({ content: "there was an error while executing this select menu!",
M src/events/messageCreate.tssrc/events/messageCreate.ts

@@ -1,220 +1,12 @@

-import config from "@/config"; -import { GuildModel, UserModel } from "@/database/schemas"; -import lily from "@/utils/logging"; -import vision from "@google-cloud/vision"; -import { - ActionRowBuilder, - type Attachment, - ButtonBuilder, - ButtonStyle, - ChannelType, - type Client, - Events, - type Message, -} from "discord.js"; - -type ImageResult = { - image: Attachment; - isCat: boolean; -}; - -const MS_IN_DAY = 1000 * 60 * 60 * 24; -const REPORT_COOLDOWN_DAYS = 28; -const REPORT_COOLDOWN = MS_IN_DAY * REPORT_COOLDOWN_DAYS; - -const logger = lily.child(["google-cloud", "vision"]); - -type ImageResults = Record<string, ImageResult>; -const client = new vision.ImageAnnotatorClient({ - projectId: config.data.googleCloud?.projectId, - credentials: { - client_email: config.data.googleCloud?.credentials.clientEmail, - private_key: config.data.googleCloud?.credentials.privateKey, - }, -}); - -async function processImage( - message: Message, -): Promise<ImageResults | undefined> { - if (!config.data.googleCloud) { - logger.warn("gcp vision api not configured"); - return; - } - - const imageAttachments = message.attachments.filter((attachment) => - attachment.contentType?.startsWith("image/"), - ); - - if (imageAttachments.size === 0) return; - - const results: ImageResults = {}; - for (const [id, attachment] of imageAttachments) { - try { - const response = await fetch(attachment.url); - if (!response.ok) { - logger.error(`failed to download image ${attachment.url}`); - results[id] = { image: attachment, isCat: false }; - continue; - } - - const buffer = await response.arrayBuffer(); - - if (client.objectLocalization === undefined) { - logger.warn( - "objectLocalization method not available in Google Cloud Vision client", - ); - return; - } - - const [result] = await client.objectLocalization({ - image: { content: Buffer.from(buffer) }, - }); - - const objects = result.localizedObjectAnnotations || []; - const hasCat = objects.some( - (obj) => - obj.name?.toLowerCase().includes("cat") && (obj.score || 0) > 0.5, - ); - - results[id] = { image: attachment, isCat: hasCat }; - logger.debug(`image ${attachment.name}: cat detected = ${hasCat}`); - } catch (error) { - logger.error(`error processing image ${attachment.name}:`, error); - results[id] = { image: attachment, isCat: true }; - } - } - - return results; -} - -async function handleReportChannel(message: Message) { - if (!message.guild) return; - const guildData = await GuildModel.findOne({ guild_id: message.guild.id }); - if ( - !guildData?.report_channel || - message.channel.id !== guildData.report_channel - ) { - return; - } - - let user = await UserModel.findOne({ - user_id: message.author.id, - guild_id: message.guild.id, - }); - - if (!user) { - user = new UserModel({ - user_id: message.author.id, - guild_id: message.guild.id, - }); - } else { - const lastReport = user.last_report; - if (lastReport) { - const timeSinceLastReport = Date.now() - lastReport.getTime(); - if (timeSinceLastReport < REPORT_COOLDOWN) return; - } - } - - user.last_report = new Date(); - await user.save(); - - if (guildData.report_message) { - const agreeButton = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder() - .setCustomId("report:agree") - .setLabel("I confirm that my report follows the rules") - .setStyle(ButtonStyle.Primary), - ); - - await message.reply({ - content: guildData.report_message.replace( - "{user}", - `<@${message.author.id}>`, - ), - allowedMentions: { users: [message.author.id] }, - components: [agreeButton], - }); - } -} +import { type Client, Events, type Message } from "discord.js"; export default { event: Events.MessageCreate, - async execute(client: Client, message: Message) { + async execute(_client: Client, message: Message) { if (message.author.bot) return; if (message.content.includes("grok is this true")) { if (message.channel.isSendable()) await message.channel.send("yeh"); } - - await handleReportChannel(message); - - if ( - !message.attachments.some((attachment) => - attachment.contentType?.startsWith("image/"), - ) || - !config.data.catChannel?.channelId - ) - return; - - if (message.channel.id !== config.data.catChannel.channelId) return; - const channel = client.channels.cache.get(config.data.catChannel.channelId); - if ( - !channel || - !channel.isTextBased() || - channel.type !== ChannelType.GuildText - ) - return; - - if (!config.data.googleCloud) { - await message.react("😻"); - await awardCatPoints( - message.author.id, - message.guildId, - message.attachments.size, - ); - return; - } - - const results = await processImage(message); - if (!results) return; - - const allResults = Object.values(results); - const catImages = allResults.filter((r) => r.isCat); - const nonCatImages = allResults.filter((r) => !r.isCat); - - if (nonCatImages.length > 0) { - const replyContent = [ - "this channel is only for cat images!", - allResults.length > 1 - ? `i found ${nonCatImages.length} non-cat images:` - : "", - ].join(" "); - await message.reply({ - content: replyContent, - }); - } else if (catImages.length > 0) { - await message.react("😻"); - await awardCatPoints( - message.author.id, - message.guildId, - message.attachments.size, - ); - } }, }; - -async function awardCatPoints( - userId: string, - guildId: string | null, - points: number, -) { - if (!guildId) return; - const user = await UserModel.findOneAndUpdate( - { user_id: userId, guild_id: guildId }, - { $inc: { cat_points: points } }, - { upsert: true, new: true }, - ); - logger.info( - `awarded ${points} cat points to user ${userId}, total: ${user.cat_points}`, - ); -}
M src/handlers/events.tssrc/handlers/events.ts

@@ -1,9 +1,9 @@

+import _logger from "@/utils/logging.ts"; import { type Client, Collection } from "discord.js"; import type { IEvent } from "../types.ts"; -import lily from "../utils/logging.ts"; import { crawlDirectory, getHandlerPath } from "./common.ts"; -const logger = lily.child("events"); +const logger = _logger.child({ name: "events" }); async function getEvents(): Promise<Collection<string, IEvent>> { const eventFiles = new Collection<string, IEvent>();
M src/handlers/interactions.tssrc/handlers/interactions.ts

@@ -3,10 +3,10 @@ import { Collection, REST, Routes } from "discord.js";

import cfg from "@/config.ts"; import type { ICommand } from "@/types.ts"; -import lily from "@/utils/logging.ts"; +import _logger from "@/utils/logging.ts"; import { crawlDirectory, getHandlerPath } from "./common.ts"; -const logger = lily.child("interactions"); +const logger = _logger.child({ name: "interactions" }); export const commands = new Collection<string, ICommand>(); async function getCommands(): Promise<ICommand[]> {

@@ -66,7 +66,7 @@ body: interactions,

}); logger.info(`registered ${interactions.length} commands`); } catch (error) { - logger.error("failed to register commands", error); + logger.error(error, "failed to register commands"); } }
M src/index.tssrc/index.ts

@@ -1,49 +1,19 @@

-import { Client, Events, GatewayIntentBits } from "discord.js"; - -import { connectDatabase } from "./database/connection"; -import registerEvents from "./handlers/events.ts"; -import registerInteractions from "./handlers/interactions.ts"; - import config from "./config.ts"; -import { watchForum } from "./utils/forumWatcher.ts"; import logger from "./utils/logging.ts"; -const client = new Client({ - intents: [ - GatewayIntentBits.GuildMessages, - GatewayIntentBits.Guilds, - GatewayIntentBits.MessageContent, - ], -}); - -client.on(Events.ClientReady, async (client) => { - logger.info(`logged in as ${client.user?.tag}!`); - - await connectDatabase(); - await Promise.all([registerEvents(client), registerInteractions(client)]); - logger.info("events and interactions registered!"); - console.log(); +import client from "./bot.ts"; - const forumConfig = config.data.roblox?.forumWatcher; - const forumChannels = forumConfig?.channels ?? []; - if (!forumConfig || !forumConfig.enabled) return; - await watchForum(client); - setInterval( - () => { - watchForum(client).catch((error) => { - logger.error("error while watching forum:", error); - }); - }, - forumConfig.interval * 1000 || 60000, - ); -}); +async function main() { + await client.login(config.data.discord.token); +} -await client.login(config.data.discord.token); +main(); process.on("unhandledRejection", (error) => { - logger.error("unhandled promise rejection:", error); + logger.error(error, "unhandled promise rejection:"); }); process.on("uncaughtException", (error) => { - logger.error("uncaught exception:", error); + logger.error(error, "uncaught exception"); + console.error(error); });
A src/interactions/buttons/tickets/message.ts

@@ -0,0 +1,79 @@

+import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type ChatInputCommandInteraction, + type Client, + type ModalSubmitInteraction, + SlashCommandBuilder, +} from "discord.js"; + +import { db, guild as guildTable, manager as managerTable } from "@/database"; +import { loggers } from "@/utils/logging"; + +const logger = loggers.interactions.child({ name: "messageCommand" }); + +const MODAL_ID = "message:report_message"; +const INPUT_ID = "message_content"; + +const commandData = new SlashCommandBuilder() + .setName("ticket-message") + .setDescription("Send the ticket message in the designated channel") + .addChannelOption((c) => + c + .setName("channel") + .setDescription("The channel to send the ticket message in") + .setRequired(true), + ); + +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { + if (!interaction.guild || !interaction.member) { + await interaction.reply({ + content: "❌ this command can only be used in a server.", + flags: ["Ephemeral"], + }); + return; + } + + const channel = interaction.options.getChannel("channel"); + if (!channel) { + await interaction.reply({ + content: "❌ Please provide a valid channel.", + flags: ["Ephemeral"], + }); + return; + } + + if (!("send" in channel)) { + await interaction.reply({ + content: "❌ This channel does not support sending messages.", + flags: ["Ephemeral"], + }); + return; + } + + const buttonRow = new ActionRowBuilder<ButtonBuilder>().addComponents( + new ButtonBuilder() + .setCustomId("ticket:create") + .setLabel("Confirm") + .setStyle(ButtonStyle.Success), + ); + + await channel.send({ + content: `Ticket message from ${interaction.user.tag}:`, + components: [buttonRow], + }); + + await interaction.reply({ + content: "✅ Ticket message sent.", + flags: ["Ephemeral"], + }); +} + +export default { + data: commandData, + execute, +};
A src/interactions/buttons/tickets/ticket.ts

@@ -0,0 +1,294 @@

+import { type Ticket, db, guild, tickets } from "@/database"; +import type { ICommand } from "@/types"; +import { formatMessage } from "@/utils/formatting"; +import { + countTickets, + getGuild, + getManagerRoleIds, + getTicket, + getTicketMessage, +} from "@/utils/queries"; +import { + ActionRowBuilder, + ButtonBuilder, + type ButtonInteraction, + ButtonStyle, + ChannelType, + type ChatInputCommandInteraction, + type Client, + ContainerBuilder, + EmbedBuilder, + type Interaction, + LabelBuilder, + type ModalSubmitInteraction, + PermissionFlagsBits, + SectionBuilder, + SlashCommandBuilder, + SlashCommandSubcommandBuilder, + TextDisplayBuilder, + TextInputBuilder, + TextInputStyle, +} from "discord.js"; +import { ModalBuilder } from "discord.js"; +import { eq } from "drizzle-orm"; + +const commandData = new SlashCommandBuilder() + .setName("ticket") + .setDescription("Manage and create new tickets") + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("create") + .setDescription("Create a new ticket"), + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("message") + .setDescription("Set the initial message sent in new tickets"), + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("close") + .setDescription("Close the ticket in the current channel"), + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("reopen") + .setDescription("Reopen the ticket in the current channel"), + ) + .addSubcommand( + new SlashCommandSubcommandBuilder() + .setName("delete") + .setDescription("Delete the ticket in the current channel"), + ); + +async function getModal(guildId: string) { + const { data: message } = await getTicketMessage(guildId); + + const modal = new ModalBuilder() + .setCustomId("ticket:message") + .setTitle("Edit Report Message"); + + const label = new LabelBuilder() + .setLabel("Report Message Content") + .setDescription("This will be the message shown to first time reporters.") + .setTextInputComponent( + new TextInputBuilder() + .setCustomId("ticket:input") + .setPlaceholder("meow meow :3") + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(2000) + .setRequired(true) + .setValue(message ?? ""), + ); + + modal.addLabelComponents(label); + return modal; +} + +// ==================================== +// interaction handlers +// ==================================== +async function execute( + client: Client, + interaction: ChatInputCommandInteraction, +) { + const action = interaction.options.getSubcommand(); + + if (!interaction.channelId || !interaction.guildId) { + await interaction.reply({ + content: "This command can only be used in a server.", + }); + return; + } + + if (action === "message") { + const modal = await getModal(interaction.guildId); + await interaction.showModal(modal); + return; + } + + await interaction.deferReply({ flags: ["Ephemeral"] }); + + if (action === "create") await handleCreate(client, interaction); + + const ticket = await getTicket(interaction.guildId, interaction.channelId); +} + +export async function buttonExecute( + client: Client, + interaction: ButtonInteraction, +) { + await interaction.deferReply({ flags: ["Ephemeral"] }); + const [action] = interaction.customId.split(":").slice(1); + + if (!interaction.channelId || !interaction.guildId) { + await interaction.editReply({ + content: "This command can only be used in a server.", + }); + return; + } + + if (action === "create") await handleCreate(client, interaction); + + const ticket = await getTicket(interaction.guildId, interaction.channelId); +} + +async function modalExecute( + _client: Client, + interaction: ModalSubmitInteraction, +) { + await interaction.deferReply({ flags: ["Ephemeral"] }); + if (!interaction.guildId) { + await interaction.editReply({ + content: "This command can only be used in a server.", + }); + return; + } + + const action = interaction.customId.split(":")[1]; + + if (action === "message") { + const message = interaction.fields.getTextInputValue("ticket:input"); + + await db + .update(guild) + .set({ ticket_message: message }) + .where(eq(guild.guildId, interaction.guildId)); + + await interaction.editReply({ + content: "Ticket message updated successfully!", + }); + } +} + +// ==================================== +// action handlers +// ==================================== +async function handleCreate( + client: Client, + interaction: ChatInputCommandInteraction | ButtonInteraction, +) { + if (!interaction.guild) throw new Error("Guild is not available"); + if (!client.user) throw new Error("Client user is not available"); + + const { data: guild, exists } = await getGuild(interaction.guild.id); + if (!exists) { + await interaction.editReply({ + content: "This server hasn't been setup yet.", + }); + return; + } + + const anonymousId = crypto.randomUUID().split("-")[0]; + const managerRoleIds = await getManagerRoleIds(interaction.guild.id); + + const ticketCount = await countTickets(guild.guildId); + const paddedTicketCount = (ticketCount + 1).toString().padStart(3, "0"); + + const channel = await interaction.guild.channels.create({ + name: `ticket-${paddedTicketCount}`, + type: ChannelType.GuildText, + parent: guild.ticket_category_id, + permissionOverwrites: [ + { + id: interaction.guild.id, + deny: [PermissionFlagsBits.ViewChannel], + }, + { + id: interaction.user.id, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.AttachFiles, + ], + }, + { + id: client.user.id, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.ManageChannels, + ], + }, + ...managerRoleIds.map((roleId) => ({ + id: roleId, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages, + PermissionFlagsBits.AttachFiles, + ], + })), + ], + }); + + const _ticket = ( + await db + .insert(tickets) + .values({ + guildId: interaction.guild.id, + channelId: channel.id, + authorId: interaction.user.id, + anonymousId: anonymousId, + type: "general", + status: "open", + }) + .returning() + )[0]; + + const formatted = formatMessage(guild.ticket_message ?? "", interaction); + + const embed = new EmbedBuilder() + .setColor("#ff5a00") + .setTitle("Ticket created!") + .setDescription(formatMessage(guild.ticket_message ?? "", interaction)) + .setFooter({ text: `ref: ${anonymousId}` }); + + const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( + new ButtonBuilder() + .setCustomId("ticket:close") + .setLabel("Close ticket") + .setStyle(ButtonStyle.Danger) + .setEmoji("🔏"), + ); + + const container = new ContainerBuilder() + .setAccentColor(0xff5a00) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent("# Ticket Created!"), + new TextDisplayBuilder().setContent(formatted), + ) + .addActionRowComponents(controls); + + await channel.send({ + components: [container], + flags: ["IsComponentsV2"], + }); + + await interaction.editReply({ + content: `:white_check_mark: Ticket created ${channel.toString()}`, + }); +} + +async function handleDelete( + _client: Client, + interaction: ButtonInteraction, + ticket: Ticket, +) {} + +async function handleClose( + _client: Client, + interaction: ButtonInteraction, + ticket: Ticket, +) {} + +async function handleReopen( + _client: Client, + interaction: ButtonInteraction, + ticket: Ticket, +) {} + +export default { + data: commandData, + execute, + buttonExecute, + modalExecute, +};
D src/interactions/commands/bug/buttons.ts

@@ -1,289 +0,0 @@

-import config from "@/config.ts"; -import { BugModel, GuildModel, MediaModel } from "@/database/schemas.ts"; -import lily from "@/utils/logging.ts"; -import { hasManagerPermissions } from "@/utils/permissions"; -import { - ActionRowBuilder, - ButtonBuilder, - type ButtonInteraction, - ButtonStyle, - ChannelType, - type Client, - type GuildMember, - type Interaction, -} from "discord.js"; -import { showEditModal } from "./report.ts"; - -const logger = lily.child("bugButtons"); - -export function buildTrelloUrl(title: string, url: string): string { - const params = new URLSearchParams({ - name: title, - url: url, - idBoard: config.data?.trelloBoardId || "", - }); - return `https://trello.com/addCard?${params.toString()}`; -} - -export function buildButtonRow( - bugId: number, - messageUrl: string, - bugTitle: string, - isClosed: boolean, -): ActionRowBuilder<ButtonBuilder> { - const row = new ActionRowBuilder<ButtonBuilder>(); - - if (isClosed) { - row.addComponents( - new ButtonBuilder() - .setCustomId(`bug:open:${bugId}`) - .setLabel("open") - .setStyle(ButtonStyle.Secondary), - ); - } else { - row.addComponents( - new ButtonBuilder() - .setCustomId(`bug:close:${bugId}`) - .setLabel("close") - .setStyle(ButtonStyle.Secondary), - ); - } - - row.addComponents( - new ButtonBuilder() - .setCustomId(`bug:edit:${bugId}`) - .setLabel("edit") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(`bug:delete:${bugId}`) - .setLabel("delete") - .setStyle(ButtonStyle.Danger), - ); - - if (config.data.trelloBoardId) { - row.addComponents( - new ButtonBuilder() - .setLabel("add to trello") - .setStyle(ButtonStyle.Link) - .setURL(buildTrelloUrl(bugTitle, messageUrl)), - ); - } - - return row; -} - -export async function canManageBug( - interaction: Interaction, - bugUserId: string, -): Promise<boolean> { - if (!interaction.guild || !interaction.member) return false; - if (interaction.user.id === bugUserId) return true; - - const guildData = await GuildModel.findOne({ - guild_id: interaction.guild.id, - }); - if (!guildData) return false; - - return await hasManagerPermissions(interaction.member as GuildMember); -} - -export async function handleCloseButton( - client: Client, - interaction: ButtonInteraction, -) { - const bugId = Number.parseInt(interaction.customId.split(":")[2]); - const bug = await BugModel.findOne({ bug_id: bugId }); - - if (!bug) { - await interaction.reply({ - content: "❌ Bug report not found.", - flags: ["Ephemeral"], - }); - return; - } - - const canManage = await canManageBug(interaction, bug.user_id); - if (!canManage) { - await interaction.reply({ - content: "❌ You don't have permission to close this bug report.", - flags: ["Ephemeral"], - }); - return; - } - - bug.status = "closed"; - await bug.save(); - - if (bug.message_id && bug.thread_id) { - const thread = await client.channels.fetch(bug.thread_id); - if (thread?.isThread()) { - const message = await thread.fetchStarterMessage(); - if (message) { - const buttonRow = buildButtonRow(bugId, message.url, bug.title, true); - await message.edit({ - components: [...message.components.slice(0, -1), buttonRow], - }); - } - } - } - - await interaction.reply({ - content: "✅ Bug report closed successfully.", - flags: ["Ephemeral"], - }); - - logger.info(`Bug #${bugId} closed by ${interaction.user.id}`); -} - -export async function handleOpenButton( - client: Client, - interaction: ButtonInteraction, -) { - const bugId = Number.parseInt(interaction.customId.split(":")[2]); - const bug = await BugModel.findOne({ bug_id: bugId }); - - if (!bug) { - await interaction.reply({ - content: "❌ Bug report not found.", - flags: ["Ephemeral"], - }); - return; - } - - const canManage = await canManageBug(interaction, bug.user_id); - if (!canManage) { - await interaction.reply({ - content: "❌ You don't have permission to reopen this bug report.", - flags: ["Ephemeral"], - }); - return; - } - - bug.status = "open"; - await bug.save(); - - if (bug.message_id && bug.thread_id) { - const thread = await client.channels.fetch(bug.thread_id); - if (thread?.isThread()) { - const message = await thread.fetchStarterMessage(); - if (message) { - const buttonRow = buildButtonRow(bugId, message.url, bug.title, false); - await message.edit({ - components: [...message.components.slice(0, -1), buttonRow], - }); - } - } - } - - await interaction.reply({ - content: "✅ Bug report reopened successfully.", - flags: ["Ephemeral"], - }); - - logger.info(`Bug #${bugId} reopened by ${interaction.user.id}`); -} - -export async function handleEditButton( - _client: Client, - interaction: ButtonInteraction, -) { - const bugId = Number.parseInt(interaction.customId.split(":")[2]); - const bug = await BugModel.findOne({ bug_id: bugId }); - - if (!bug) { - await interaction.reply({ - content: "❌ Bug report not found.", - flags: ["Ephemeral"], - }); - return; - } - - const canManage = await canManageBug(interaction, bug.user_id); - if (!canManage) { - await interaction.reply({ - content: "❌ You don't have permission to edit this bug report.", - flags: ["Ephemeral"], - }); - return; - } - - const projectKey = bug.projects[0] || ""; - - await showEditModal( - bugId, - bug.title, - bug.description, - projectKey, - interaction, - ); -} - -export async function handleDeleteButton( - client: Client, - interaction: ButtonInteraction, -) { - const bugId = Number.parseInt(interaction.customId.split(":")[2]); - const bug = await BugModel.findOne({ bug_id: bugId }); - - if (!bug) { - await interaction.reply({ - content: "❌ Bug report not found.", - flags: ["Ephemeral"], - }); - return; - } - - const canManage = await canManageBug(interaction, bug.user_id); - if (!canManage) { - await interaction.reply({ - content: "❌ You don't have permission to delete this bug report.", - flags: ["Ephemeral"], - }); - return; - } - - await interaction.deferReply({ flags: ["Ephemeral"] }); - - try { - // delete associated media - const media = await MediaModel.find({ bug_id: bugId }); - if (media.length > 0) { - await MediaModel.deleteMany({ bug_id: bugId }); - logger.info(`deleted ${media.length} media files for bug #${bugId}`); - } - - // delete associated message & thread - if (bug.message_id && bug.thread_id) { - const thread = await client.channels.fetch(bug.thread_id); - if (thread?.type !== ChannelType.PublicThread) return; - - const message = await thread.fetchStarterMessage(); - if (message) await message.delete(); - - try { - await thread.delete( - `bug report #${bugId} deleted by ${interaction.user.username}`, - ); - } catch (threadError) { - logger.warn(`Failed to delete thread for bug #${bugId}:`, threadError); - } - } - - // tombstone - bug.status = "closed"; - bug.title = "[DELETED]"; - bug.description = "[DELETED]"; - await bug.save(); - - await interaction.editReply({ - content: "✅ Bug report deleted successfully.", - }); - - logger.info(`Bug #${bugId} deleted by ${interaction.user.id}`); - } catch (error) { - logger.error(`Failed to delete bug #${bugId}:`, error); - await interaction.editReply({ - content: "❌ Failed to delete bug report. Please try again later.", - }); - } -}
D src/interactions/commands/bug/help.ts

@@ -1,17 +0,0 @@

-import type { ChatInputCommandInteraction, Client } from "discord.js"; - -export async function execute( - _client: Client, - interaction: ChatInputCommandInteraction, -) { - const tgtUser = interaction.options.getUser("user", true); - - const command = `</bug report:${interaction.commandId}>`; - await interaction.reply({ - content: `📝 <@${tgtUser.id}>, you can use the ${command} command to report bugs, it will show a popup where you can enter the bug details and attach a relevant piece of media.`, - }); -} - -export const helpCommand = { - execute, -};
D src/interactions/commands/bug/index.ts

@@ -1,82 +0,0 @@

-import type { ICommand } from "@/types.ts"; -import { - type ApplicationCommandData, - type ButtonInteraction, - type Client, - SlashCommandBuilder, -} from "discord.js"; -import { - handleCloseButton, - handleDeleteButton, - handleEditButton, - handleOpenButton, -} from "./buttons.ts"; -import { helpCommand } from "./help.ts"; -import { reportCommand } from "./report.ts"; - -const commandData = new SlashCommandBuilder() - .setName("bug") - .setDescription("bug report management") - .addSubcommand((subcommand) => - subcommand.setName("report").setDescription("report a new bug"), - ) - .addSubcommand((subcommand) => - subcommand - .setName("help") - .setDescription("help a user report a bug") - .addUserOption((option) => - option - .setName("user") - .setDescription("the user to send the button to") - .setRequired(true), - ), - ); - -async function buttonExecute(client: Client, interaction: ButtonInteraction) { - const [, action] = interaction.customId.split(":"); - - switch (action) { - case "close": - await handleCloseButton(client, interaction); - break; - case "open": - await handleOpenButton(client, interaction); - break; - case "edit": - await handleEditButton(client, interaction); - break; - case "delete": - await handleDeleteButton(client, interaction); - break; - default: - await interaction.reply({ - content: "❌ Unknown button action.", - flags: ["Ephemeral"], - }); - } -} - -export default { - data: commandData.toJSON() as ApplicationCommandData, - execute: async (client, interaction) => { - if (!interaction.isChatInputCommand()) return; - - const subcommand = interaction.options.getSubcommand(); - - switch (subcommand) { - case "report": - await reportCommand.execute(client, interaction); - break; - case "help": - await helpCommand.execute(client, interaction); - break; - default: - await interaction.reply({ - content: "❌ unknown subcommand.", - flags: ["Ephemeral"], - }); - } - }, - modalExecute: reportCommand.modalExecute, - buttonExecute, -} satisfies ICommand;
D src/interactions/commands/bug/report.ts

@@ -1,682 +0,0 @@

-import config from "@/config.ts"; -import { - BugModel, - GuildModel, - MediaModel, - type MediaType, - getNextBugId, -} from "@/database/schemas.ts"; -import { createUserIfNotExists } from "@/utils/exists.ts"; -import { - type Attachment, - AttachmentBuilder, - type ChatInputCommandInteraction, - type Client, - ComponentType, - ContainerBuilder, - FileUploadBuilder, - LabelBuilder, - MediaGalleryBuilder, - MediaGalleryItemBuilder, - ModalBuilder, - type ModalSubmitInteraction, - type ReadonlyCollection, - SectionBuilder, - type Snowflake, - StringSelectMenuBuilder, - TextDisplayBuilder, - TextInputBuilder, - TextInputStyle, - ThumbnailBuilder, -} from "discord.js"; - -import { buildButtonRow, canManageBug } from "./buttons.ts"; -import { PROJECT_MAP, logger as lily } from "./shared.ts"; - -const logger = lily.child("report"); - -// constants -const INPUT_IDS = { - AFFECTED: "affectedInput", - TITLE: "titleInput", - DESCRIPTION: "descriptionInput", - MEDIA: "fileUploadInput", -} as const; - -const MODAL_IDS = { - REPORT: "bug:report", - EDIT: "bug:edit", -} as const; - -const VALIDATION = { - TITLE_MAX_LENGTH: 100, - DESCRIPTION_MAX_LENGTH: 1000, - THREAD_NAME_MAX_LENGTH: 50, - AUTO_ARCHIVE_DURATION: 1440, -} as const; - -// types -type ProjectChoice<T extends "name" | "label" = "name"> = T extends "name" - ? { name: string; value: string } - : { label: string; value: string; default: boolean }; - -interface DownloadedMediaResult { - media: MediaType[]; - totalSize: number; -} - -// utils -function formatFileSize(bytes: number): string { - return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; -} - -function truncateTitle(title: string, maxLength: number): string { - return title.length > maxLength - ? `${title.substring(0, maxLength)}...` - : title; -} - -// media handling -async function downloadAttachment( - attachment: Attachment, - bugId: number, - userId: Snowflake, -): Promise<MediaType> { - if (!attachment.contentType) { - throw new Error(`Attachment ${attachment.id} has no content type`); - } - - try { - const response = await fetch(attachment.url); - if (!response.ok) { - throw new Error(`Failed to download attachment: ${response.statusText}`); - } - - const buffer = await response.arrayBuffer(); - const [mainType, subType] = attachment.contentType.split("/"); - const type = mainType === "image" ? "image" : "video"; - const extension = subType.split(";")[0]; - - const media = new MediaModel({ - bug_id: bugId, - user_id: userId, - media_type: type, - data: Buffer.from(buffer), - extension, - }); - - await media.save(); - return media; - } catch (error) { - logger.error("Failed to download attachment", { - error, - attachmentId: attachment.id, - bugId, - }); - throw error; - } -} - -async function downloadAttachments( - attachments: ReadonlyCollection<string, Attachment>, - bugId: number, - userId: Snowflake, -): Promise<DownloadedMediaResult> { - const totalSize = Array.from(attachments.values()).reduce( - (acc, att) => acc + att.size, - 0, - ); - - const mediaPromises = attachments.map((attachment) => - downloadAttachment(attachment, bugId, userId), - ); - - const media = await Promise.all(mediaPromises); - - return { media, totalSize }; -} - -// project choices -export function getProjectChoices<T extends "name" | "label" = "name">( - label: T = "name" as T, - defaultProjectKey?: string, -): ProjectChoice<T>[] { - return Object.entries(PROJECT_MAP).map(([key, project]) => - label === "name" - ? { name: project.displayName, value: key } - : { - label: project.displayName, - value: key, - default: key === defaultProjectKey, - }, - ) as ProjectChoice<T>[]; -} - -// modal builders -function buildReportModal(): ModalBuilder { - const terminology = config.data.terminology; - const capitalizedTerminology = - terminology.charAt(0).toUpperCase() + terminology.slice(1); - - const gameLabel = new LabelBuilder() - .setLabel(capitalizedTerminology) - .setDescription(`The ${terminology} affected by this bug`) - .setStringSelectMenuComponent( - new StringSelectMenuBuilder() - .setMaxValues(1) - .setMinValues(1) - .setCustomId(INPUT_IDS.AFFECTED) - .setPlaceholder(`Select the ${terminology} you encountered the bug in`) - .addOptions(getProjectChoices("label")), - ); - - const titleLabel = new LabelBuilder() - .setLabel("Bug Title") - .setDescription("Provide a short description of the bug") - .setTextInputComponent( - new TextInputBuilder() - .setCustomId(INPUT_IDS.TITLE) - .setStyle(TextInputStyle.Short) - .setRequired(true) - .setMaxLength(VALIDATION.TITLE_MAX_LENGTH), - ); - - const descriptionLabel = new LabelBuilder() - .setLabel("Bug Description") - .setDescription( - "Provide a detailed description of the bug and steps to reproduce", - ) - .setTextInputComponent( - new TextInputBuilder() - .setCustomId(INPUT_IDS.DESCRIPTION) - .setStyle(TextInputStyle.Paragraph) - .setRequired(true) - .setMaxLength(VALIDATION.DESCRIPTION_MAX_LENGTH), - ); - - const mediaLabel = new LabelBuilder() - .setLabel("Media") - .setDescription( - "Provide any screenshots or videos that demonstrate the bug", - ) - .setFileUploadComponent( - new FileUploadBuilder().setCustomId(INPUT_IDS.MEDIA).setRequired(false), - ); - - return new ModalBuilder() - .setCustomId(MODAL_IDS.REPORT) - .setTitle("Report a Bug") - .setLabelComponents(gameLabel, titleLabel, descriptionLabel, mediaLabel); -} - -function buildEditModal( - bugId: number, - currentTitle: string, - currentDescription: string, - currentProject: string, -): ModalBuilder { - const terminology = config.data.terminology; - const capitalizedTerminology = - terminology.charAt(0).toUpperCase() + terminology.slice(1); - - const projectOptions = getProjectChoices("label", currentProject); - const selectedProject = projectOptions.find( - (opt) => opt.value === currentProject, - ); - - const gameLabel = new LabelBuilder() - .setLabel(capitalizedTerminology) - .setDescription(`The ${terminology} affected by this bug`) - .setStringSelectMenuComponent( - new StringSelectMenuBuilder() - .setMaxValues(1) - .setMinValues(1) - .setCustomId(INPUT_IDS.AFFECTED) - .setPlaceholder(selectedProject?.label || `Select the ${terminology}`) - .addOptions(projectOptions), - ); - - const titleLabel = new LabelBuilder() - .setLabel("Bug Title") - .setDescription("Update the bug title") - .setTextInputComponent( - new TextInputBuilder() - .setCustomId(INPUT_IDS.TITLE) - .setStyle(TextInputStyle.Short) - .setRequired(true) - .setMaxLength(VALIDATION.TITLE_MAX_LENGTH) - .setValue(currentTitle), - ); - - const descriptionLabel = new LabelBuilder() - .setLabel("Bug Description") - .setDescription("Update the bug description") - .setTextInputComponent( - new TextInputBuilder() - .setCustomId(INPUT_IDS.DESCRIPTION) - .setStyle(TextInputStyle.Paragraph) - .setRequired(true) - .setMaxLength(VALIDATION.DESCRIPTION_MAX_LENGTH) - .setValue(currentDescription), - ); - - return new ModalBuilder() - .setCustomId(`${MODAL_IDS.EDIT}:${bugId}`) - .setTitle(`Edit Bug #${bugId}`) - .setLabelComponents(gameLabel, titleLabel, descriptionLabel); -} - -// validation -async function validateGuildSetup(interaction: ModalSubmitInteraction) { - if (!interaction.guild) { - throw new Error("This command can only be used in a server."); - } - - const guildData = await GuildModel.findOne({ - guild_id: interaction.guild.id, - }); - - if (!guildData) - throw new Error("Guild data not found. Please configure the bot first."); - if (!guildData.bug_channel) - throw new Error("The bug channel is not configured."); - - const channel = interaction.guild.channels.cache.get(guildData.bug_channel); - if (!channel?.isTextBased() || !("send" in channel)) - throw new Error("The bug channel configured is not a valid text channel."); - - return { guildData, channel }; -} - -function parseModalCustomId(customId: string): { - type: string; - bugId?: number; -} { - const parts = customId.split(":"); - if (parts.length < 2 || parts[0] !== "bug") { - throw new Error("Invalid modal submission format."); - } - - const type = parts[1]; - const bugId = parts[2] ? Number.parseInt(parts[2]) : undefined; - - return { type, bugId }; -} - -// containers -function buildBugContainer( - bug: InstanceType<typeof BugModel>, - projectKey: string, - userId: Snowflake, - mediaUrls: string[], -): ContainerBuilder { - const projectInfo = PROJECT_MAP[projectKey]; - if (!projectInfo) { - throw new Error(`Project ${projectKey} not found in PROJECT_MAP`); - } - - const textContent = `### ${bug.title}\n${bug.description}`; - const footerContent = [ - `-# #${bug.bug_id}`, - projectInfo.displayName, - `Reported by <@${userId}>`, - ].join(" • "); - - const container = new ContainerBuilder().addSectionComponents( - new SectionBuilder() - .setThumbnailAccessory( - new ThumbnailBuilder().setURL(projectInfo.iconURL || ""), - ) - .addTextDisplayComponents( - new TextDisplayBuilder().setContent(textContent), - ), - ); - - if (mediaUrls.length > 0) { - container.addMediaGalleryComponents( - new MediaGalleryBuilder().addItems( - ...mediaUrls.map((url) => new MediaGalleryItemBuilder().setURL(url)), - ), - ); - } - - container.addTextDisplayComponents( - new TextDisplayBuilder().setContent(footerContent), - ); - - return container; -} - -// modals handlers -async function handleEditModal( - client: Client, - interaction: ModalSubmitInteraction, - bugId: number, -): Promise<void> { - if (!interaction.guild) { - await interaction.reply({ - content: "❌ This command can only be used in a server.", - flags: ["Ephemeral"], - }); - return; - } - - const bug = await BugModel.findOne({ bug_id: bugId }); - if (!bug) { - await interaction.reply({ - content: "❌ Bug report not found.", - flags: ["Ephemeral"], - }); - return; - } - - const canManage = await canManageBug(interaction, bug.user_id); - if (!canManage) { - await interaction.reply({ - content: "❌ You don't have permission to edit this bug report.", - flags: ["Ephemeral"], - }); - return; - } - - await interaction.deferReply({ flags: ["Ephemeral"] }); - - try { - // collect inputs - const newTitle = interaction.fields.getTextInputValue(INPUT_IDS.TITLE); - const newDescription = interaction.fields.getTextInputValue( - INPUT_IDS.DESCRIPTION, - ); - const newProject = interaction.fields - .getStringSelectValues(INPUT_IDS.AFFECTED) - .join(""); - - // track changes - const changes: string[] = []; - if (bug.title !== newTitle) changes.push("title"); - if (bug.description !== newDescription) changes.push("description"); - if (bug.projects.join(",") !== newProject) changes.push("project"); - - if (changes.length === 0) { - await interaction.editReply({ - content: "❌ No changes were made to the bug report.", - }); - return; - } - - bug.title = newTitle; - bug.description = newDescription; - bug.projects = [newProject]; - await bug.save(); - - if (bug.message_id && bug.thread_id) { - const thread = await client.channels.fetch(bug.thread_id); - if (thread?.isThread()) { - const message = await thread.fetchStarterMessage(); - if (message) { - const projectKey = newProject.split(",")[0]; - - const existingMediaUrls = Array.from(message.components.values()) - .filter((comp) => comp.type === ComponentType.Container) - .flatMap((comp) => Array.from(comp.components.values())) - .filter((item) => item.type === ComponentType.MediaGallery) - .flatMap((item) => Array.from(item.items.values())) - .map((mediaItem) => mediaItem.media.url); - - // build updated container & edit message - const storedMedia = await MediaModel.find({ bug_id: bugId }); - const container = buildBugContainer( - bug, - projectKey, - bug.user_id, - existingMediaUrls, - ); - - const buttonRow = buildButtonRow( - bugId, - message.url, - bug.title, - bug.status === "closed", - ); - - const mediaAttachments = storedMedia.map((media) => - new AttachmentBuilder(media.data).setName( - `bug_${bugId}_media.${media.extension}`, - ), - ); - - await message.edit({ - components: [container, buttonRow], - files: mediaAttachments.length > 0 ? mediaAttachments : undefined, - }); - - // update thread name if title changed - if (changes.includes("title")) { - const newThreadName = `#${bugId} ${truncateTitle(bug.title, VALIDATION.THREAD_NAME_MAX_LENGTH)}`; - await thread.setName(newThreadName); - } - - const editLogMessage = `Bug report edited by <@${interaction.user.id}> • **Changes:** ${changes.join(", ")} • <t:${Math.floor(Date.now() / 1000)}:R>`; - await thread.send({ - content: editLogMessage, - allowedMentions: { users: [] }, - }); - } - } - } - - await interaction.editReply({ - content: `✅ Bug report #${bugId} has been updated successfully!\n**Changed:** ${changes.join(", ")}`, - }); - - logger.info("Bug report edited successfully", { - bugId, - editedBy: interaction.user.id, - changes, - }); - } catch (error) { - logger.error("Failed to edit bug report", { error, bugId }); - - const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred."; - - await interaction.editReply({ - content: `❌ ${errorMessage}`, - }); - } -} - -// main -export async function execute( - _client: Client, - interaction: ChatInputCommandInteraction, -): Promise<void> { - const modal = buildReportModal(); - await interaction.showModal(modal); -} - -// modal submission handling -export async function modalExecute( - client: Client, - interaction: ModalSubmitInteraction, -): Promise<void> { - try { - if (!interaction.guild) { - await interaction.reply({ - content: "❌ This command can only be used in a server.", - flags: ["Ephemeral"], - }); - return; - } - - const { type, bugId: _bugId } = parseModalCustomId(interaction.customId); - - if (type === "edit") { - if (!_bugId) { - await interaction.reply({ - content: "❌ Invalid bug ID.", - flags: ["Ephemeral"], - }); - return; - } - await handleEditModal(client, interaction, _bugId); - return; - } - - const { channel } = await validateGuildSetup(interaction); - await createUserIfNotExists(interaction.user.id, interaction.guild.id); - - // collect inputs & defer - const title = interaction.fields.getTextInputValue(INPUT_IDS.TITLE); - const description = interaction.fields.getTextInputValue( - INPUT_IDS.DESCRIPTION, - ); - const affected = interaction.fields - .getStringSelectValues(INPUT_IDS.AFFECTED) - .join(""); - const mediaFiles = interaction.fields.getUploadedFiles(INPUT_IDS.MEDIA); - - await interaction.deferReply({ flags: ["Ephemeral"] }); - - // create record - const bugId = await getNextBugId(); - const bug = new BugModel({ - bug_id: bugId, - guild_id: interaction.guild.id, - user_id: interaction.user.id, - title, - description, - projects: affected, - status: "open", - sent: false, - }); - await bug.save(); - - logger.info("Bug report created", { - bugId, - userId: interaction.user.id, - guildId: interaction.guild.id, - }); - - // handle media uploads - let downloadedMedia: MediaType[] = []; - if (mediaFiles && mediaFiles.size > 0) { - const { media, totalSize } = await downloadAttachments( - mediaFiles, - bugId, - interaction.user.id, - ); - - logger.info("Downloading attachments", { - bugId, - userId: interaction.user.id, - fileCount: mediaFiles.size, - totalSize, - }); - - await interaction.editReply({ - content: `⏳ Downloading ${mediaFiles.size} attachment(s) (${formatFileSize(totalSize)})... This may take a moment.`, - }); - - downloadedMedia = media; - } - - // build message container - const projectKey = affected.split(",")[0]; - const mediaUrls = mediaFiles - ? Array.from(mediaFiles.values()).map((att) => att.url) - : []; - const container = buildBugContainer( - bug, - projectKey, - interaction.user.id, - mediaUrls, - ); - - // send message to bug channel - const message = await channel.send({ - components: [container], - flags: "IsComponentsV2", - files: downloadedMedia.map((media) => - new AttachmentBuilder(media.data).setName( - `bug_${bugId}_media.${media.extension}`, - ), - ), - allowedMentions: { users: [] }, - }); - - // create thread - const threadName = `#${bugId} ${truncateTitle(title, VALIDATION.THREAD_NAME_MAX_LENGTH)}`; - const thread = await message.startThread({ - name: threadName, - autoArchiveDuration: VALIDATION.AUTO_ARCHIVE_DURATION, - reason: `Thread for bug report #${bugId}`, - }); - - await thread.send({ - content: - "Use this space to discuss the bug, provide additional details, or ask questions.", - }); - await thread.members.add(interaction.user.id); - - // update bug with message and thread IDs, add buttons - bug.message_id = message.id; - bug.thread_id = thread.id; - await bug.save(); - - const buttonRow = buildButtonRow(bugId, message.url, title, false); - await message.edit({ - components: [container, buttonRow], - }); - - // final success message - await interaction.editReply({ - content: `✅ Bug report #${bugId} has been submitted successfully! Check <#${channel.id}> for your report.`, - }); - - logger.info("Bug report submitted successfully", { - bugId, - messageId: message.id, - threadId: thread.id, - }); - } catch (error) { - logger.error("Failed to process bug report", { error }); - - const errorMessage = - error instanceof Error ? error.message : "An unknown error occurred."; - - if (interaction.deferred || interaction.replied) { - await interaction.editReply({ - content: `❌ ${errorMessage}`, - }); - } else { - await interaction.reply({ - content: `❌ ${errorMessage}`, - flags: ["Ephemeral"], - }); - } - } -} - -export async function showEditModal( - bugId: number, - currentTitle: string, - currentDescription: string, - currentProject: string, - interaction: { showModal: (modal: ModalBuilder) => Promise<void> }, -): Promise<void> { - const modal = buildEditModal( - bugId, - currentTitle, - currentDescription, - currentProject, - ); - await interaction.showModal(modal); -} - -export const reportCommand = { - execute, - modalExecute, - getProjectChoices, - showEditModal, -};
D src/interactions/commands/bug/shared.ts

@@ -1,75 +0,0 @@

-import config from "@/config.ts"; -import type { BugType } from "@/database/schemas.ts"; -import lily from "@/utils/logging.ts"; -import type { Client } from "discord.js"; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - EmbedBuilder, -} from "discord.js"; - -export const logger = lily.child("bugShared"); -export const PROJECT_MAP = config.data.projects; - -export function getProjectName(value: string): string { - const project = PROJECT_MAP[value as keyof typeof PROJECT_MAP]; - if (!project) return `unknown ${config.data.terminology}`; - return project.name; -} - -export async function updateBugEmbed( - client: Client, - bug: BugType, - messageId: string, - channelId: string, -): Promise<void> { - try { - const channel = await client.channels.fetch(channelId); - if (!channel?.isTextBased() || !("messages" in channel)) return; - - const message = await channel.messages.fetch(messageId); - const projectInfo = PROJECT_MAP[bug.project as keyof typeof PROJECT_MAP]; - - const embed = new EmbedBuilder() - .setAuthor({ - name: message.embeds[0].author?.name || "unknown user", - url: message.embeds[0].author?.url, - iconURL: message.embeds[0].author?.iconURL, - }) - .setTitle(bug.title) - .setColor(bug.status === "closed" ? 0x95a5a6 : 0xff6b6b) - .setDescription(bug.description) - .setFooter({ - text: `${projectInfo.name} • bug #${bug.bug_id} • ${bug.status}`, - }) - .setTimestamp(); - - if (projectInfo.iconURL) embed.setThumbnail(projectInfo.iconURL); - - const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder() - .setCustomId(`bug:close:${bug.bug_id}`) - .setLabel(bug.status === "closed" ? "reopen" : "close") - .setStyle(ButtonStyle.Secondary) - .setEmoji(bug.status === "closed" ? "🔓" : "🔒"), - new ButtonBuilder() - .setCustomId(`bug:edit:${bug.bug_id}`) - .setLabel("edit") - .setStyle(ButtonStyle.Primary) - .setDisabled(bug.status === "closed"), - new ButtonBuilder() - .setCustomId(`bug:delete:${bug.bug_id}`) - .setLabel("delete") - .setStyle(ButtonStyle.Danger), - new ButtonBuilder() - .setCustomId("bug:new") - .setLabel("new bug") - .setStyle(ButtonStyle.Success), - ); - - await message.edit({ embeds: [embed], components: [buttons] }); - } catch (error) { - logger.error("failed to update bug embed:", error); - } -}
D src/interactions/commands/catlb.ts

@@ -1,191 +0,0 @@

-import { UserModel } from "@/database/schemas"; -import { - ActionRowBuilder, - ButtonBuilder, - type ButtonInteraction, - ButtonStyle, - type ChatInputCommandInteraction, - type Client, - ContainerBuilder, - SeparatorBuilder, - SlashCommandBuilder, - TextDisplayBuilder, -} from "discord.js"; - -const PAGE_SIZE = 10; - -const commandData = new SlashCommandBuilder() - .setName("cat-leaderboard") - .setDescription("show the top cat point earners in this server"); - -type SessionState = { - userId: string; - currentPage: number; - pageCount: number; - userIds: string[]; -}; - -const sessionStore = new Map<string, SessionState>(); - -function makeSessionId(): string { - return `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; -} - -function makeButtonRow( - sessionId: string, - currentPage: number, - pageCount: number, -) { - return new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder() - .setCustomId(`cat-leaderboard:prev:${sessionId}`) - .setLabel("previous") - .setStyle(ButtonStyle.Secondary) - .setDisabled(currentPage === 1), - - new ButtonBuilder() - .setCustomId(`cat-leaderboard:next:${sessionId}`) - .setLabel("next") - .setStyle(ButtonStyle.Secondary) - .setDisabled(currentPage === pageCount || pageCount === 0), - ); -} - -function buildLeaderboardContainer( - users: { user_id: string; cat_points: number }[], - currentPage: number, - pageCount: number, -) { - const startRank = (currentPage - 1) * PAGE_SIZE + 1; - const lines = users.map( - (user, i) => - `${startRank + i}. <@${user.user_id}> — ${user.cat_points} cat point${user.cat_points === 1 ? "" : "s"}`, - ); - - const container = new ContainerBuilder() - .setAccentColor([245, 197, 66]) - .addTextDisplayComponents( - new TextDisplayBuilder().setContent("# 🐱 cat points leaderboard"), - new TextDisplayBuilder().setContent( - lines.length > 0 - ? lines.join("\n") - : "no cat points have been awarded yet!", - ), - ) - .addSeparatorComponents( - new SeparatorBuilder().setDivider(false).setSpacing(1), - ) - .addTextDisplayComponents( - new TextDisplayBuilder().setContent( - `-# page ${currentPage} of ${pageCount} • last updated: <t:${Math.floor(Date.now() / 1000)}:R>`, - ), - ); - - return container; -} - -async function execute( - _client: Client, - interaction: ChatInputCommandInteraction, -) { - if (!interaction.guild) { - await interaction.reply({ - content: "❌ this command can only be used in a server.", - flags: ["Ephemeral"], - }); - return; - } - - const allUsers = await UserModel.find({ - guild_id: interaction.guildId, - cat_points: { $exists: true, $ne: null }, - }) - .sort({ cat_points: -1, user_id: 1 }) - .select("user_id cat_points") - .lean(); - - const pageCount = Math.max(1, Math.ceil(allUsers.length / PAGE_SIZE)); - const currentPage = 1; - const users = allUsers.slice(0, PAGE_SIZE); - - const sessionId = makeSessionId(); - sessionStore.set(sessionId, { - userId: interaction.user.id, - currentPage, - pageCount, - userIds: allUsers.map((u) => u.user_id), - }); - - const container = buildLeaderboardContainer(users, currentPage, pageCount); - const actions = makeButtonRow(sessionId, currentPage, pageCount); - - await interaction.reply({ - flags: ["IsComponentsV2"], - components: [container, actions], - allowedMentions: { users: [] }, - }); -} - -async function buttonExecute(_client: Client, interaction: ButtonInteraction) { - const [, action, sessionId] = interaction.customId.split(":"); - const session = sessionStore.get(sessionId); - - if (!session) { - return interaction.reply({ - content: - "❌ this leaderboard session has expired. Please run the command again.", - flags: ["Ephemeral"], - }); - } - if (interaction.user.id !== session.userId) { - return interaction.reply({ - content: `❌ only <@${session.userId}> can interact with this leaderboard.`, - flags: ["Ephemeral"], - }); - } - - let newPage = session.currentPage; - if (action === "prev") newPage = Math.max(1, session.currentPage - 1); - else if (action === "next") - newPage = Math.min(session.pageCount, session.currentPage + 1); - - if (newPage !== session.currentPage) { - session.currentPage = newPage; - sessionStore.set(sessionId, session); - - const allUsers = await UserModel.find({ - guild_id: interaction.guildId, - cat_points: { $exists: true, $ne: null }, - }) - .sort({ cat_points: -1, user_id: 1 }) - .select("user_id cat_points") - .lean(); - - session.pageCount = Math.max(1, Math.ceil(allUsers.length / PAGE_SIZE)); - const users = allUsers.slice( - (newPage - 1) * PAGE_SIZE, - newPage * PAGE_SIZE, - ); - - const container = buildLeaderboardContainer( - users, - newPage, - session.pageCount, - ); - const actions = makeButtonRow(sessionId, newPage, session.pageCount); - - await interaction.update({ - flags: ["IsComponentsV2"], - components: [container, actions], - allowedMentions: { users: [] }, - }); - } else { - await interaction.deferUpdate(); - } -} - -export default { - data: commandData, - execute, - buttonExecute, -};
D src/interactions/commands/codes.ts

@@ -1,172 +0,0 @@

-import config from "@/config"; -import { - ActionRowBuilder, - ButtonBuilder, - type ButtonInteraction, - ButtonStyle, - type ChatInputCommandInteraction, - type Client, - ContainerBuilder, - SlashCommandBuilder, - TextDisplayBuilder, -} from "discord.js"; - -import { getProjectChoices } from "@/utils/choices"; -import { formatTime } from "@/utils/time"; - -const commandData = new SlashCommandBuilder() - .setName("codes") - .setDescription(`get the codes for the associated ${config.data.terminology}`) - .addStringOption((option) => - option - .setName(config.data.terminology) - .setDescription("the project to get the codes for") - .setRequired(true) - .addChoices(getProjectChoices()), - ) - .addUserOption((option) => - option - .setName("user") - .setDescription("the user to send the codes to") - .setRequired(false), - ); - -async function execute( - _client: Client, - interaction: ChatInputCommandInteraction, -) { - const projectKey = interaction.options.getString( - config.data.terminology, - true, - ); - const project = config.data.projects[projectKey]; - - if (!project) { - return interaction.reply({ - content: `couldn't find data for ${config.data.terminology} "${projectKey}"`, - flags: ["Ephemeral"], - }); - } - - const codes = project.codes || []; - const container = new ContainerBuilder(); - const user = interaction.options.getUser("user"); - const p2 = user ? `• <@${user.id}>` : ""; - - if (codes.length === 0) { - const textComponents = [ - new TextDisplayBuilder().setContent( - `# no codes for ${project.displayName}`, - ), - new TextDisplayBuilder().setContent( - `there aren't any known codes for this ${config.data.terminology} at the moment`, - ), - ]; - - const footerComponent = new TextDisplayBuilder().setContent( - `please tell <@${config.data.developerId}> if this is incorrect ${p2}`, - ); - - container.addTextDisplayComponents(textComponents); - container.addTextDisplayComponents(footerComponent); - return interaction.reply({ - components: [container], - flags: ["IsComponentsV2"], - ...(user - ? { allowedMentions: { users: [user?.id] } } - : { allowedMentions: { users: [] } }), - }); - } - - { - const codeButtons = codes.map((code) => - new ButtonBuilder() - .setCustomId(`codes:${projectKey}:${code.code}`) - .setLabel(code.code) - .setStyle(code.expired ? ButtonStyle.Danger : ButtonStyle.Success), - ); - - const buttonRows: ActionRowBuilder<ButtonBuilder>[] = []; - for (let i = 0; i < codeButtons.length; i += 4) { - const row = new ActionRowBuilder<ButtonBuilder>().addComponents( - codeButtons.slice(i, i + 4), - ); - buttonRows.push(row); - } - - const textComponent = new TextDisplayBuilder().setContent( - `# codes for ${project.displayName}`, - ); - - const footerComponent = new TextDisplayBuilder().setContent( - `please tell <@${config.data.developerId}> if any any missing or expired codes ${p2}`, - ); - - container.addTextDisplayComponents(textComponent); - container.addActionRowComponents(buttonRows); - container.addTextDisplayComponents(footerComponent); - } - - return interaction.reply({ - components: [container], - flags: ["IsComponentsV2"], - ...(user - ? { allowedMentions: { users: [user?.id] } } - : { allowedMentions: { users: [] } }), - }); -} - -async function buttonExecute(client: Client, interaction: ButtonInteraction) { - const [, project, code] = interaction.customId.split(":"); - if (!project || !code) { - return interaction.reply({ - content: `invalid code button interaction, missing either ${config.data.terminology} or code`, - flags: ["Ephemeral"], - }); - } - - const projectData = config.data.projects[project]; - if (!projectData) { - return interaction.reply({ - content: `couldn't find data for ${config.data.terminology} "${project}"`, - flags: ["Ephemeral"], - }); - } - - const codeData = projectData.codes?.find((c) => c.code === code); - if (!codeData) { - return interaction.reply({ - content: `couldn't find code "${code}" for ${config.data.terminology} "${project}"`, - flags: ["Ephemeral"], - }); - } - - return interaction.reply({ - content: `\`${codeData.code}\``, - flags: ["Ephemeral"], - components: [ - new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder() - .setCustomId("disabled:0") - .setLabel(codeData.expired ? "expired" : "active") - .setStyle(codeData.expired ? ButtonStyle.Danger : ButtonStyle.Success) - .setDisabled(true), - new ButtonBuilder() - .setCustomId("disabled:1") - .setLabel( - codeData.addedAt - ? `added at ${formatTime(codeData.addedAt)}` - : "added at unknown time", - ) - .setStyle(ButtonStyle.Secondary) - .setDisabled(true), - ), - ], - }); -} - -export default { - data: commandData, - execute, - buttonExecute, -};
M src/interactions/commands/config.tssrc/interactions/commands/config.ts

@@ -1,25 +1,44 @@

-import config from "@/config.ts"; -import { GuildModel, type GuildType } from "@/database/schemas.ts"; -import lily from "@/utils/logging.ts"; -import { - ChannelType, - type ChatInputCommandInteraction, - type Client, - PermissionFlagsBits, - SlashCommandBuilder, +import { SlashCommandBuilder } from "discord.js"; +import type { + AutocompleteInteraction, + ChatInputCommandInteraction, + Client, + GuildMember, } from "discord.js"; +import { ChannelType, type GuildChannel, type Role } from "discord.js"; -const logger = lily.child("configCommand"); +import cfg from "@/config"; +import { db, guild as guildTable, manager as managerTable } from "@/database"; +import { loggers } from "@/utils/logging"; +import { hasManagerPermissions } from "@/utils/permissions"; +import { getGuild } from "@/utils/queries"; +import { eq } from "drizzle-orm"; + +const logger = loggers.interactions.child({ name: "configCommand" }); + +const CONFIG_KEYS = [ + "suggestion_forum_id", + "bug_channel_id", + "highlights_channel_id", + "commands_channel_id", + "ticket_channel_id", + "ticket_category_id", + "ticket_message", +] as const; + +function isChannelKey(key: string) { + return key.endsWith("_id"); +} const commandData = new SlashCommandBuilder() .setName("config") .setDescription("configure bot settings for this server") - .addSubcommand((subcommand) => - subcommand + .addSubcommand((sub) => + sub .setName("manager-role") - .setDescription("manage roles that can manage bug reports") - .addStringOption((option) => - option + .setDescription("manage roles that can manage reports/tickets") + .addStringOption((opt) => + opt .setName("action") .setDescription("action to perform") .setRequired(true)

@@ -29,52 +48,55 @@ { name: "remove", value: "remove" },

{ name: "list", value: "list" }, ), ) - .addRoleOption((option) => - option - .setName("role") - .setDescription("role to add/remove") - .setRequired(false), + .addRoleOption((opt) => + opt.setName("role").setDescription("role to add/remove"), ), ) - .addSubcommand((subcommand) => - subcommand - .setName("bug-channel") - .setDescription("set the channel where bug reports are sent") - .addChannelOption((option) => - option + .addSubcommand((sub) => + sub + .setName("set") + .setDescription("set a guild configuration key") + .addStringOption((opt) => + opt + .setName("key") + .setDescription("configuration key to set") + .setRequired(true) + .setAutocomplete(true), + ) + .addChannelOption((opt) => + opt .setName("channel") - .setDescription("channel for bug reports") - .setRequired(true), + .setDescription("channel to set (for channel keys)") + .setRequired(false), + ) + .addStringOption((opt) => + opt + .setName("value") + .setDescription("value to set (for non-channel keys)") + .setRequired(false), ), ) - .addSubcommand((subcommand) => - subcommand - .setName("highlight-channel") - .setDescription("set the channel where highlights are sent") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("channel for highlights") - .setRequired(true), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("report-channel") - .setDescription("set the channel where report are to be sent") - .addChannelOption((option) => - option - .setName("channel") - .setDescription("channel for report") - .setRequired(true), - ), + .addSubcommand((sub) => + sub.setName("view").setDescription("view current server configuration"), ); +async function autocomplete(interaction: AutocompleteInteraction) { + const focused = interaction.options.getFocused(true); + if (focused.name === "key") { + const input = String(focused.value).toLowerCase(); + const suggestions = CONFIG_KEYS.filter((k) => + k.toLowerCase().includes(input), + ).slice(0, 25); + + await interaction.respond(suggestions.map((s) => ({ name: s, value: s }))); + } +} + async function execute( _client: Client, interaction: ChatInputCommandInteraction, ) { - if (!interaction.guild) { + if (!interaction.guild || !interaction.member) { await interaction.reply({ content: "❌ this command can only be used in a server.", flags: ["Ephemeral"],

@@ -82,79 +104,80 @@ });

return; } - if ( - !interaction.memberPermissions || - (!interaction.memberPermissions.has(PermissionFlagsBits.ManageGuild) && - interaction.user.id !== config.data.developerId) - ) { + const isDev = interaction.user.id === cfg.data.developerId; + const isManager = hasManagerPermissions(interaction.member as GuildMember); + + if (!(isDev || isManager)) { await interaction.reply({ - content: "❌ you do not have permission to manage this server.", + content: "❌ You do not have permission to manage this server.", flags: ["Ephemeral"], }); return; } - const subcommand = interaction.options.getSubcommand(); + const sub = interaction.options.getSubcommand(); try { - let guild = await GuildModel.findOne({ - guild_id: interaction.guild.id, - }); - - if (!guild) { - guild = new GuildModel({ - guild_id: interaction.guild.id, - manager_roles: [], - }); - await guild.save(); - logger.info(`created new guild record for ${interaction.guild.id}`); - } - - if (subcommand === "manager-role") { - await handleManagerRole(interaction, guild); - } else if (subcommand === "bug-channel") { - await handleBugChannel(interaction, guild); - } else if (subcommand === "highlight-channel") { - await handleHighlightChannel(interaction, guild); - } else if (subcommand === "report-channel") { - await handleReportChannel(interaction, guild); + if (sub === "manager-role") { + await handleManagerRole(interaction); + } else if (sub === "set") { + await handleSet(interaction); + } else if (sub === "view") { + await handleView(interaction); } else { await interaction.reply({ - content: "❌ unknown subcommand.", + content: "❌ Unknown subcommand.", flags: ["Ephemeral"], }); } - } catch (error) { - logger.error("failed to execute config command:", error); + } catch (err) { + logger.error(err, "Failed to execute config command"); await interaction.reply({ - content: "❌ failed to update configuration. please try again later.", + content: "❌ Failed to update configuration, please try again later.", flags: ["Ephemeral"], }); } } -async function handleManagerRole( - interaction: ChatInputCommandInteraction, - guild: GuildType, -) { +async function handleManagerRole(interaction: ChatInputCommandInteraction) { + if (!interaction.guild || !interaction.member) { + await interaction.reply({ + content: "❌ This command can only be used in a server.", + flags: ["Ephemeral"], + }); + return; + } + const action = interaction.options.getString("action", true); - const role = interaction.options.getRole("role"); + const role = interaction.options.getRole("role") as Role | null; + const guildId = interaction.guild.id; + + if (!(await hasManagerPermissions(interaction.member as GuildMember))) { + await interaction.reply({ + content: "❌ You do not have manager permissions to run this action.", + flags: ["Ephemeral"], + }); + return; + } if (action === "list") { - if (guild.manager_roles.length === 0) { + const rows = await db + .select({ roleId: managerTable.roleId }) + .from(managerTable) + .where(eq(managerTable.guildId, guildId)) + .execute(); + + if (!rows || rows.length === 0) { await interaction.reply({ - content: "📋 no manager roles configured.", + content: "📋 There are no manager roles configured for this server.", flags: ["Ephemeral"], }); return; } - const roleList = guild.manager_roles - .map((roleId: string) => `<@&${roleId}>`) - .join("\n"); - + const mentions = rows.map((r) => `\* <@&${r.roleId}>`).join("\n"); await interaction.reply({ - content: `📋 **manager roles:**\n${roleList}`, + content: `**📋 Manager roles**\n${mentions}`, flags: ["Ephemeral"], }); return;

@@ -162,115 +185,173 @@ }

if (!role) { await interaction.reply({ - content: "❌ you must specify a role for this action.", + content: "❌ You must specify a role for this action.", flags: ["Ephemeral"], }); return; } if (action === "add") { - if (guild.manager_roles.includes(role.id)) { + const existing = await db + .select() + .from(managerTable) + .where(eq(managerTable.roleId, role.id)) + .execute(); + + if (existing && existing.length > 0) { await interaction.reply({ - content: `❌ ${role} is already a manager role.`, + content: `❌ ${role} is already configured as a manager role.`, flags: ["Ephemeral"], }); return; } - guild.manager_roles.push(role.id); - await guild.save(); + await db + .insert(managerTable) + .values({ + roleId: role.id, + guildId, + }) + .execute(); await interaction.reply({ content: `✅ added ${role} as a manager role.`, flags: ["Ephemeral"], }); - } else if (action === "remove") { - const index = guild.manager_roles.indexOf(role.id); - if (index === -1) { - await interaction.reply({ - content: `❌ ${role} is not a manager role.`, - flags: ["Ephemeral"], - }); - return; - } + return; + } - guild.manager_roles.splice(index, 1); - await guild.save(); + if (action === "remove") { + await db + .delete(managerTable) + .where(eq(managerTable.roleId, role.id)) + .execute(); await interaction.reply({ content: `✅ removed ${role} from manager roles.`, flags: ["Ephemeral"], }); + return; } + + await interaction.reply({ + content: "❌ unknown action.", + flags: ["Ephemeral"], + }); } -async function handleBugChannel( - interaction: ChatInputCommandInteraction, - guild: GuildType, -) { - const channel = interaction.options.getChannel("channel", true); +async function handleSet(interaction: ChatInputCommandInteraction) { + if (!interaction.guild) { + await interaction.reply({ + content: "❌ This command can only be used in a server.", + flags: ["Ephemeral"], + }); + return; + } + + const key = interaction.options.getString("key", true); - if (channel.type !== ChannelType.GuildText) { + // @ts-expect-error shush + if (!CONFIG_KEYS.includes(key)) { await interaction.reply({ - content: "❌ bug channel must be a text channel.", + content: "❌ Unknown configuration key.", flags: ["Ephemeral"], }); return; } - guild.bug_channel = channel.id; - await guild.save(); + const guildId = interaction.guild.id; + + // use getGuild to ensure a row exists + await getGuild(guildId); + + if (isChannelKey(key)) { + const channel = interaction.options.getChannel( + "channel", + false, + ) as GuildChannel | null; - await interaction.reply({ - content: `✅ set bug reports channel to ${channel}.`, - flags: ["Ephemeral"], - }); -} + if (!channel) { + await interaction.reply({ + content: "❌ this key expects a channel - please provide a channel.", + flags: ["Ephemeral"], + }); + return; + } + + const allowedTypes = [ + ChannelType.GuildText, + ChannelType.GuildAnnouncement, + ChannelType.GuildCategory, + ]; + if (!allowedTypes.includes(channel.type)) { + await interaction.reply({ + content: "❌ Channel must be a text channel or category.", + flags: ["Ephemeral"], + }); + return; + } -async function handleHighlightChannel( - interaction: ChatInputCommandInteraction, - guild: GuildType, -) { - const channel = interaction.options.getChannel("channel", true); + await db + .update(guildTable) + .set({ [key]: channel.id }) + .where(eq(guildTable.guildId, guildId)) + .execute(); - const allowedTypes = [ChannelType.GuildText, ChannelType.GuildAnnouncement]; - if (!allowedTypes.includes(channel.type)) { await interaction.reply({ - content: "❌ highlight channel must be a text channel.", + content: `✅ Set \`${key}\` to <#${channel.id}>.`, flags: ["Ephemeral"], }); return; } - guild.highlights_channel = channel.id; - await guild.save(); + const value = interaction.options.getString("value", true); + await db + .update(guildTable) + .set({ [key]: value }) + .where(eq(guildTable.guildId, guildId)) + .execute(); await interaction.reply({ - content: `✅ set highlights channel to ${channel}.`, + content: `✅ set ${key} to \`${value}\`.`, flags: ["Ephemeral"], }); } -async function handleReportChannel( - interaction: ChatInputCommandInteraction, - guild: GuildType, -) { - const channel = interaction.options.getChannel("channel", true); +async function handleView(interaction: ChatInputCommandInteraction) { + if (!interaction.guild) { + await interaction.reply({ + content: "❌ This command can only be used in a server.", + flags: ["Ephemeral"], + }); + return; + } - const allowedTypes = [ChannelType.GuildText, ChannelType.GuildAnnouncement]; - if (!allowedTypes.includes(channel.type)) { + const guildId = interaction.guild.id; + const result = await db + .select() + .from(guildTable) + .where(eq(guildTable.guildId, guildId)) + .execute(); + + if (!result || result.length === 0) { await interaction.reply({ - content: "❌ report channel must be a text channel.", + content: "📋 no configuration found for this server.", flags: ["Ephemeral"], }); return; } - guild.report_channel = channel.id; - await guild.save(); + const row = result[0] as Record<string, string>; + const lines = CONFIG_KEYS.map((k) => { + const val = row[k]; + if (!val) return `- \`${k}\` - unset`; + if (isChannelKey(k)) return `- \`${k}\` <#${val}>`; + return `- \`${k}\` ${val}`; + }); await interaction.reply({ - content: `✅ set report channel to ${channel}.`, + content: `# 📋 Server Configuration\n${lines.join("\n")}`, flags: ["Ephemeral"], }); }

@@ -278,4 +359,5 @@

export default { data: commandData, execute, + autocomplete, };
D src/interactions/commands/message.ts

@@ -1,129 +0,0 @@

-import { GuildModel } from "@/database/schemas"; -import lily from "@/utils/logging"; -import { hasManagerPermissions } from "@/utils/permissions"; -import { - ActionRowBuilder, - type ChatInputCommandInteraction, - type Client, - type GuildMember, - LabelBuilder, - ModalBuilder, - type ModalSubmitInteraction, - SlashCommandBuilder, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; - -const logger = lily.child("messageCommand"); - -const MODAL_ID = "message:report_message"; -const INPUT_ID = "message_content"; - -const commandData = new SlashCommandBuilder() - .setName("message") - .setDescription("set the custom report message shown in this server"); - -async function execute( - _client: Client, - interaction: ChatInputCommandInteraction, -) { - if (!interaction.guild || !interaction.member) { - await interaction.reply({ - content: "❌ this command can only be used in a server.", - flags: ["Ephemeral"], - }); - return; - } - - const isManager = await hasManagerPermissions( - interaction.member as GuildMember, - ); - if (!isManager) { - await interaction.reply({ - content: "❌ you do not have permission to manage this server.", - flags: ["Ephemeral"], - }); - return; - } - - try { - const guildData = await GuildModel.findOne({ - guild_id: interaction.guild.id, - }); - - const currentMessage = guildData?.report_message || ""; - - const modal = new ModalBuilder() - .setCustomId(MODAL_ID) - .setTitle("Edit Report Message"); - - const label = new LabelBuilder() - .setLabel("Report Message Content") - .setDescription("This will be the message shown to first time reporters.") - .setTextInputComponent( - new TextInputBuilder() - .setCustomId(INPUT_ID) - .setPlaceholder("meow meow :3") - .setStyle(TextInputStyle.Paragraph) - .setMaxLength(2000) - .setRequired(true) - .setValue(currentMessage), - ); - - modal.addLabelComponents(label); - await interaction.showModal(modal); - } catch (error) { - logger.error("failed to open message modal:", error); - await interaction.reply({ - content: "❌ an error occurred while trying to open the configuration.", - flags: ["Ephemeral"], - }); - } -} - -async function modalExecute( - _client: Client, - interaction: ModalSubmitInteraction, -) { - console.log("modalExecute called"); - if (interaction.customId !== MODAL_ID) return; - - if (!interaction.guild) { - await interaction.reply({ - content: "❌ this command can only be used in a server.", - flags: ["Ephemeral"], - }); - return; - } - - await interaction.deferReply({ flags: ["Ephemeral"] }); - - try { - const newMessage = interaction.fields.getTextInputValue(INPUT_ID); - - await GuildModel.findOneAndUpdate( - { guild_id: interaction.guild.id }, - { report_message: newMessage }, - { upsert: true, new: true }, - ); - - logger.info( - `updated report message for guild ${interaction.guild.id} by ${interaction.user.id}`, - ); - - await interaction.editReply({ - content: "✅ report message updated successfully.", - }); - } catch (error) { - logger.error("failed to save report message:", error); - await interaction.editReply({ - content: "❌ failed to save the report message. please try again later.", - }); - } -} - -export default { - data: commandData, - execute, - modalExecute, -};
D src/interactions/commands/ping.ts

@@ -1,53 +0,0 @@

-import { - type ChatInputCommandInteraction, - type Client, - SlashCommandBuilder, -} from "discord.js"; - -const commandData = new SlashCommandBuilder() - .setName("ping") - .setDescription("ping the bot"); - -async function execute( - client: Client, - interaction: ChatInputCommandInteraction, -) { - const pingMs = Date.now() - interaction.createdTimestamp; - - const initial = await interaction.reply({ - content: "calc'ing latency information...", - flags: ["Ephemeral"], - }); - - const roundTripMs = Date.now() - initial.createdTimestamp; - const wsHeartbeat = client.ws.ping; - - return interaction.editReply({ - content: - `time to receive was ${pingMs}ms, ` + - `round trip is ${roundTripMs}ms, ` + - `ws heartbeat is ${wsHeartbeat}ms, ` + - `uptime is ${formatUptime(client.uptime || 0)}`, - }); -} - -function formatUptime(uptime: number): string { - const totalSeconds = Math.floor(uptime / 1000); - const days = Math.floor(totalSeconds / 86400); - const hours = Math.floor((totalSeconds % 86400) / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - const seconds = totalSeconds % 60; - - const parts = []; - if (days > 0) parts.push(`${days}d`); - if (hours > 0) parts.push(`${hours}h`); - if (minutes > 0) parts.push(`${minutes}m`); - if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`); - - return parts.join(" "); -} - -export default { - data: commandData, - execute, -};
D src/interactions/commands/report.ts

@@ -1,36 +0,0 @@

-import type { ICommand } from "@/types"; -import { - ActionRowBuilder, - ButtonBuilder, - type ButtonInteraction, - ButtonStyle, - type Client, -} from "discord.js"; - -export async function buttonExecute( - client: Client, - interaction: ButtonInteraction, -) { - const message = interaction.message; - await interaction.reply({ - content: "Thank you.", - flags: ["Ephemeral"], - }); - - const agreedButton = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder() - .setCustomId("report:agreed") - .setLabel("I confirm that my report follows the rules") - .setDisabled(true) - .setStyle(ButtonStyle.Success), - ); - - await message.edit({ - content: message.content, - components: [agreedButton], - }); -} - -export default { - buttonExecute, -} satisfies ICommand;
D src/interactions/commands/search.ts

@@ -1,156 +0,0 @@

-import { - ActionRowBuilder, - ButtonBuilder, - type ButtonInteraction, - ButtonStyle, - type ChatInputCommandInteraction, - type Client, - ContainerBuilder, - type GuildMember, - MediaGalleryBuilder, - MediaGalleryItemBuilder, - SlashCommandBuilder, - TextDisplayBuilder, - escapeMarkdown, -} from "discord.js"; - -import { getRobloxUser, searchRobloxUsers } from "@/utils/roblox"; -import { formatUserInfo } from "@/utils/userInfo"; - -const commandData = new SlashCommandBuilder() - .setName("search") - .setDescription("search for roblox users") - .addStringOption((option) => - option - .setName("query") - .setDescription("the username to search for") - .setRequired(true), - ) - .addIntegerOption((option) => - option - .setName("limit") - .setDescription("number of results to show (1-25)") - .setMinValue(1) - .setMaxValue(25) - .setRequired(false), - ); - -async function execute( - _client: Client, - interaction: ChatInputCommandInteraction, -) { - const query = interaction.options.getString("query", true); - const limit = interaction.options.getInteger("limit") || 10; - - await interaction.deferReply(); - - const { - users: usersResult, - error, - code, - } = await searchRobloxUsers(query, limit); - - if (error) { - if (code === 429) - return interaction.editReply({ - content: "❌ ran into a rate limit, wait a minute or so and retry", - }); - - return interaction.editReply({ - content: `❌ error while searching: ${error}`, - }); - } - - if (usersResult.length === 0) { - await interaction.editReply({ - content: `❌ no users found for "${query}"`, - }); - return; - } - - const userContainers = Promise.all( - usersResult.map(async (user) => { - const userId = user.id; - const userResult = await getRobloxUser(String(userId), 75); - if (!userResult || "code" in userResult.user) return null; - - const container = new ContainerBuilder().setAccentColor([203, 166, 247]); - const avatarParent = new MediaGalleryBuilder(); - const avatar = userResult.thumbnail.done - ? new MediaGalleryItemBuilder().setURL( - userResult.thumbnail.response.imageUri, - ) - : null; - - const title = new TextDisplayBuilder().setContent( - `## ${userResult.user.name} (${userResult.user.id})`, - ); - - const description = new TextDisplayBuilder().setContent( - escapeMarkdown(userResult.user.about || "no description provided"), - ); - - const profileLink = new ButtonBuilder() - .setLabel("view profile") - .setStyle(ButtonStyle.Link) - .setURL(`https://www.roblox.com/users/${userResult.user.id}/profile`); - const showMore = new ButtonBuilder() - .setLabel("show more") - .setStyle(ButtonStyle.Primary) - .setCustomId(`search:${userResult.user.id}`); - - const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents( - profileLink, - showMore, - ); - - if (avatar) - container.addMediaGalleryComponents(avatarParent.addItems(avatar)); - container.addTextDisplayComponents(title, description); - container.addActionRowComponents(actionRow); - - return container; - }), - ); - - const usersData = await userContainers; - const nonNull = usersData.filter((userData) => userData !== null); - - const MAX_COMPONENTS = 40; - const MAX_USERS = MAX_COMPONENTS / 8; - if (nonNull.length > MAX_USERS) nonNull.length = Math.floor(MAX_USERS); - - await interaction.followUp({ - components: nonNull, - flags: ["IsComponentsV2"], - }); -} - -async function buttonExecute(_client: Client, interaction: ButtonInteraction) { - if (!interaction.isButton() || !interaction.member) return; - - const [action, userId] = interaction.customId.split(":"); - if (action !== "search") return; - - const { user, thumbnail } = await getRobloxUser(userId, 420); - if (!user || "code" in user) - return interaction.reply({ - content: `❌ failed to fetch user with id ${userId}: ${user?.message || "unknown error"}`, - flags: ["Ephemeral"], - }); - - const { embed } = await formatUserInfo( - user, - thumbnail.done ? thumbnail.response.imageUri : null, - interaction.member as GuildMember, - ); - - await interaction.reply({ embeds: [embed], flags: ["Ephemeral"] }); -} - -export default { - enabled: true, - data: commandData, - execute, - buttonExecute, -};
D src/interactions/commands/user.ts

@@ -1,102 +0,0 @@

-import { - type ChatInputCommandInteraction, - type Client, - SlashCommandBuilder, -} from "discord.js"; - -import config from "@/config"; -import { buildBansContainer } from "@/utils/bans"; -import { hasManagerPermissions } from "@/utils/permissions"; -import { - getUserInfoFromDiscord, - getUserInfoFromRoblox, - getUserInfoFromRobloxUsername, -} from "@/utils/userInfo"; - -const commandData = new SlashCommandBuilder() - .setName("user") - .setDescription("get roblox user information") - .addSubcommand((subcommand) => - subcommand - .setName("discord") - .setDescription("get roblox info from a discord user") - .addUserOption((option) => - option - .setName("user") - .setDescription("the discord user to look up") - .setRequired(true), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("roblox") - .setDescription("get roblox info from a user id or username") - .addStringOption((option) => - option - .setName("input") - .setDescription("the roblox username to lookup - prefix with id: for IDs") - .setRequired(true), - ), - ); - -async function execute( - _client: Client, - interaction: ChatInputCommandInteraction, -) { - const subcommand = interaction.options.getSubcommand(); - - await interaction.deferReply(); - - let result: Awaited<ReturnType<typeof getUserInfoFromDiscord>>; - if (!interaction.member) { - await interaction.editReply({ - content: "❌ you must be in a server to use this command.", - }); - return; - } - - if (subcommand === "discord") { - const discordUser = interaction.options.getUser("user", true); - result = await getUserInfoFromDiscord(discordUser.id, interaction.member); - } else if (subcommand === "roblox") { - let input = interaction.options.getString("input", true); - const isId = input.startsWith("id:") - input = isId ? input.slice(3) : input; - - if (isId) result = await getUserInfoFromRoblox(input, interaction.member); - else result = await getUserInfoFromRobloxUsername(input, interaction.member); - } else { - await interaction.editReply({ - content: "❌ unknown subcommand.", - }); - return; - } - - if ("error" in result) { - await interaction.editReply({ - content: `❌ ${result.error}`, - }); - return; - } - - if ("code" in result.user) { - await interaction.editReply({ - content: `❌ failed to fetch user: ${result.user.message}`, - }); - return; - } - - await interaction.editReply({ - flags: ["IsComponentsV2"], - components: [...result.containers, ...result.actionRows], - }); - await interaction.followUp({ - content: result.user.id, - }); -} - -export default { - enabled: config.data.roblox?.apiKey && (config.data.bloxlink?.token || true), - data: commandData, - execute, -};
D src/interactions/menus/getInfo.ts

@@ -1,56 +0,0 @@

-import { - ApplicationCommandType, - type Client, - ContextMenuCommandBuilder, - type UserContextMenuCommandInteraction, -} from "discord.js"; - -import config from "@/config"; -import { getUserInfoFromDiscord } from "@/utils/userInfo"; - -const commandData = new ContextMenuCommandBuilder() - .setName("get account") - .setType(ApplicationCommandType.User); - -async function execute( - _client: Client, - interaction: UserContextMenuCommandInteraction, -) { - if (!interaction.member) { - await interaction.reply({ - content: "❌ you must be in a server to use this command.", - flags: ["Ephemeral"], - }); - return; - } - - const result = await getUserInfoFromDiscord( - interaction.targetId, - interaction.member, - ); - - if ("error" in result) { - return interaction.reply({ - content: `❌ ${result.error}`, - flags: ["Ephemeral"], - }); - } - if ("code" in result.user) { - return interaction.reply({ - content: `❌ error fetching user info: ${result.user.message}`, - flags: ["Ephemeral"], - }); - } - - await interaction.reply({ - flags: ["IsComponentsV2"], - components: [...result.containers, ...result.actionRows], - }); - await interaction.followUp({ content: result.user.id }); -} - -export default { - enabled: config.data.bloxlink?.token && config.data.roblox?.apiKey, - data: commandData, - execute, -};
D src/interactions/menus/highlight.ts

@@ -1,125 +0,0 @@

-import { GuildModel } from "@/database/schemas"; -import lily from "@/utils/logging"; -import { - ApplicationCommandType, - type Attachment, - type Client, - ContextMenuCommandBuilder, - type GuildTextBasedChannel, - type MessageContextMenuCommandInteraction, -} from "discord.js"; - -const logger = lily.child("highlightMenu"); - -const commandData = new ContextMenuCommandBuilder() - .setName("highlight") - .setType(ApplicationCommandType.Message); - -function extractVideoLinks(message: { content: string }): string[] { - const urlRegex = - /https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=[\w-]+|youtu\.be\/[\w-]+|medal\.tv\/(?:games\/[\w-]+\/clips\/[\w-]+|g\/[\w-]+|clips\/[\w-]+))/gi; - return Array.from(message.content.matchAll(urlRegex)).map((m) => m[0]); -} - -function isVideoAttachment(attachment: Attachment): boolean { - return ( - typeof attachment.contentType === "string" && - attachment.contentType.startsWith("video/") - ); -} - -async function execute( - client: Client, - interaction: MessageContextMenuCommandInteraction, -) { - if (!interaction.guild) { - await interaction.reply({ - content: "❌ this command can only be used in a server.", - ephemeral: true, - }); - return; - } - - await interaction.deferReply({ flags: ["Ephemeral"] }); - const guildConfig = await GuildModel.findOne({ - guild_id: interaction.guild.id, - }); - if (!guildConfig?.highlights_channel) { - await interaction.editReply({ - content: - "❌ no highlights channel configured. Use `/config highlight-channel`.", - }); - return; - } - - const targetMessage = interaction.targetMessage; - if (!targetMessage) { - await interaction.editReply({ - content: "❌ couldn't find the target message.", - }); - return; - } - - const videoAttachments = targetMessage.attachments.filter(isVideoAttachment); - const videoLinks = extractVideoLinks(targetMessage); - - if (videoAttachments.size === 0 && videoLinks.length === 0) { - await interaction.editReply({ - content: - "❌ no video attachments or supported links found in this message.", - }); - return; - } - - let highlightsChannel: GuildTextBasedChannel | null = null; - try { - highlightsChannel = (await client.channels.fetch( - guildConfig.highlights_channel, - )) as GuildTextBasedChannel | null; - } catch (e) { - logger.error("failed to fetch highlights channel:", e); - await interaction.editReply({ - content: "❌ could not access the highlights channel.", - }); - return; - } - - if (!highlightsChannel) { - await interaction.editReply({ - content: "❌ highlights channel not found.", - }); - return; - } - - const files = Array.from(videoAttachments.values()).map((a) => a.url); - const author = `<@${targetMessage.author.id}>`; - const jumpUrl = targetMessage.url; - - const videoLinksText = videoLinks - .map((link) => `• [video Link](${link})`) - .join(" "); - const description = `:star: new highlight from ${author}!\n-# [jump to message](${jumpUrl}) ${videoLinksText}`; - - try { - const msg = await highlightsChannel.send({ - content: description, - files: files.length > 0 ? files : undefined, - allowedMentions: { users: [targetMessage.author.id] }, - }); - - await msg.react("⭐"); - await interaction.editReply({ - content: "✅ highlight sent!", - }); - } catch (error) { - logger.error("failed to send highlight:", error); - await interaction.editReply({ - content: "❌ failed to send highlight.", - }); - } -} - -export default { - data: commandData, - execute, -};
D src/types/forums.ts

@@ -1,52 +0,0 @@

-export type ForumComment = { - id: string; - parentId: string; - content: { - plainText: string; - }; - createdBy: number; - creatorDisplayName: string | null; - createdtAt: string; - updatedAt: string | null; - deletedAt: string | null; - threadId: string | null; - reactions: []; - threadCommentCount: null | number; - threadComments: null | ForumComment[]; -}; - -export type ForumCommentCreator = { - displayName: string; - groupRoleName: string | null; - hasVerifiedBadge: boolean; -}; - -export type Post = { - categoryId: string; - isLocked: boolean; - isPinned: boolean; - isUnread: boolean; - commentCount: number; - notificationPreference: null; - id: string; - /** title of the post */ - name: string; - groupId: number; - description: string | null; - channelType: string | null; - parentChannelId: string | null; - createdBy: number; - createdAt: string; - updatedAt: string; - deletedAt: string | null; - archivedAt: string | null; - archivedBy: number | null; - /** represents the post's body */ - firstComment: ForumComment; -}; - -export type GetPosts = { - previousPageCursor: string | null; - nextPageCursor: string | null; - data: Post[]; -};
D src/types/roblox.ts

@@ -1,161 +0,0 @@

-type V2ErrorCode = - | "INVALID_ARGUMENT" - | "PERMISSION_DENIED" - | "NOT_FOUND" - | "ABORTED" - | "RESOURCE_EXHAUSTED" - | "CANCELLED" - | "INTERNAL" - | "NOT_IMPLEMENTED" - | "UNAVAILABLE"; - -interface V2ErrorResponse { - code: V2ErrorCode; - message: string; - details: Array<Record<string, unknown>>; -} - -// v1 resource err model -export type V1ErrorCode = - | "INVALID_ARGUMENT" - | "INSUFFICIENT_SCOPE" - | "PERMISSION_DENIED" - | "NOT_FOUND" - | "ABORTED" - | "RESOURCE_EXHAUSTED" - | "CANCELLED" - | "INTERNAL" - | "NOT_IMPLEMENTED" - | "UNAVAILABLE"; - -export interface V1ErrorResponse { - error: V1ErrorCode; - message: string; - errorDetails: Array<{ - errorDetailType: string; - [key: string]: unknown; - }>; -} - -// OrderedDataStores err model -export interface OrderedDataStoresErrorResponse { - code: V1ErrorCode; - message: string; -} - -// UserSearch api -export interface UserSearchParams { - keyword: string; - sessionId?: string; - limit?: 10 | 25 | 50 | 100; - cursor?: string; -} - -export type UserSearchResponse = - | UserSearchOkResponse - | UserSearchBadResponse - | UserSearchRateLimitResponse; - -export type BLApiResponse = - | { - robloxID: string; - resolved: object; - } - | { - error: "User not found"; - }; - -export type ThumbnailResponse = - | { - done: true; - response: { imageUri: string }; - } - | { - done: false; - }; - -export interface UserSearchPlayer { - previousUsernames: string[]; - hasVerifiedBadge: boolean; - id: number; - name: string; - displayName: string; -} - -export interface UserSearchOkResponse { - previousPageCursor: string | null; - nextPageCursor: string | null; - data: Array<UserSearchPlayer>; -} - -export interface UserSearchBadResponse { - statusCode: 400; - error: "INVALID_ARGUMENT"; - message: "The keyword was filtered." | "The keyword is too short."; -} - -export interface UserSearchRateLimitResponse { - statusCode: 429; - error: "RESOURCE_EXHAUSTED"; - message: "Too many requests."; -} - -export type UserSearchError = - | UserSearchBadResponse - | UserSearchRateLimitResponse; - -export interface GetUserOkResponse { - path: string; - /** RFC 3339 formatted date string */ - createTime: string; - id: string; - name: string; - displayName: string | null; - about?: string; - locale: string; - premium: boolean; - idVerified?: boolean; - socialNetworkProfiles: { - facebook?: string; - twitter?: string; - youtube?: string; - twitch?: string; - guilded?: string; - visibility: - | "SOCIAL_NETWORK_VISIBILITY_UNSPECIFIED" - | "NO_ONE" - | "FRIENDS" - | "FRIENDS_AND_FOLLOWING" - | "FRIENDS_FOLLOWING_AND_FOLLOWERS" - | "EVERYONE"; - }; -} - -export type GetUserResponse = GetUserOkResponse | V2ErrorResponse; - -export interface RobloxSearchUser { - previousUsernames: string[]; - hasVerifiedBadge: boolean; - id: number; - name: string; - displayName: string; -} - -export interface RobloxSearchResponse { - previousPageCursor: string | null; - nextPageCursor: string | null; - data: RobloxSearchUser[]; -} - -export type AvatarSize = - | 48 - | 50 - | 60 - | 75 - | 100 - | 110 - | 150 - | 180 - | 352 - | 420 - | 720;
D src/utils/bans.ts

@@ -1,105 +0,0 @@

-import config from "@/config"; -import type { GetUserOkResponse, GetUserResponse } from "@/types/roblox"; -import lily from "@/utils/logging"; -import { ContainerBuilder, TextDisplayBuilder } from "discord.js"; - -const logger = lily.child("bans"); -const projects = Object.values(config.data.projects); -const BASE_URL = "https://apis.roblox.com/user/cloud/v2/universes"; - -export type Restriction = { - active: true | false; - startTime: string; - privateReason: string; - displayReason: string; - excludeAltAccounts: boolean; - inherited: boolean; -}; - -export type GetBansResponse = { - path: string; - user: string; - gameJoinRestriction: Restriction; -}; - -export type GetBansResponseWithProject = GetBansResponse & { - project: string; -}; - -export async function getBans( - userId: string, -): Promise<GetBansResponseWithProject[] | null> { - if (!config.data.roblox?.cookie) return null; - - const fetches = projects.map(async (project) => { - const url = `${BASE_URL}/${project.universe}/user-restrictions/${userId}`; - const bans = await fetch(url, { - headers: { - Cookie: config.data.roblox?.cookie || "", - }, - }); - - if (!bans.ok) { - logger.error( - `failed to fetch bans for user ${userId} in project ${project.name}:`, - bans.statusText, - ); - return null; - } - - const data: GetBansResponse = await bans.json(); - return { ...data, project: project.displayName }; - }); - - const results = await Promise.all(fetches); - const bansArray = results.filter( - (data): data is GetBansResponseWithProject => data !== null, - ); - - return bansArray; -} - -export async function buildBansContainer( - user: GetUserOkResponse, -): Promise<ContainerBuilder> { - const bansContainer = new ContainerBuilder(); - { - const bansTitle = new TextDisplayBuilder().setContent( - `**Bans for ${user.displayName || user.name}**`, - ); - - const bans = await getBans(user.id); - - const constructBan = (ban: GetBansResponseWithProject) => { - if (!ban.gameJoinRestriction.active) { - return new TextDisplayBuilder().setContent( - `**${ban.project}**: no active ban`, - ); - } - - const restriction: Restriction = ban.gameJoinRestriction; - return new TextDisplayBuilder().setContent( - [ - `**${ban.project}**`, - `> **Active since** <t:${Math.round(new Date(restriction.startTime).getTime() / 1000)}:R>`, - `> **Display reason:** ${restriction.displayReason}`, - `> **Reason:** ${restriction.privateReason}`, - ].join("\n"), - ); - }; - - if (bans && bans.length > 0) { - const banComponents = bans.map(constructBan); - bansContainer.addTextDisplayComponents(bansTitle, ...banComponents); - } else { - bansContainer.addTextDisplayComponents( - bansTitle, - new TextDisplayBuilder().setContent( - "no active bans found for this user", - ), - ); - } - } - - return bansContainer; -}
A src/utils/constants.ts

@@ -0,0 +1,1 @@

+export const DB_FILE = "./data/db.sqlite";
D src/utils/exists.ts

@@ -1,22 +0,0 @@

-import { UserModel } from "../database/schemas"; -import lily from "../utils/logging"; - -const logger = lily.child("exists"); - -export async function createUserIfNotExists(userId: string, guildId: string) { - let user = await UserModel.findOne({ - user_id: userId, - guild_id: guildId, - }); - - if (!user) { - user = new UserModel({ - user_id: userId, - guild_id: guildId, - }); - await user.save(); - logger.info(`created new user record for ${userId}`); - } - - return user; -}
A src/utils/formatting.ts

@@ -0,0 +1,15 @@

+import type { Interaction } from "discord.js"; + +export function formatMessage( + message: string, + interaction: Interaction, +): string { + const templates: Record<string, string> = { + "{user}": `<@${interaction.user.id}>`, + }; + + return Object.entries(templates).reduce( + (msg, [key, val]) => msg.split(key).join(val), + message, + ); +}
D src/utils/forumWatcher.ts

@@ -1,247 +0,0 @@

-import config from "@/config"; -import type { Post } from "@/types/forums"; -import lily from "@/utils/logging"; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - type Client, - ContainerBuilder, - type GuildTextBasedChannel, - REST, - Routes, - TextDisplayBuilder, - inlineCode, -} from "discord.js"; -import { buildBansContainer } from "./bans"; -import { getRobloxUser } from "./roblox"; - -const startTime = Date.now(); -const seenPosts = new Set<string>(); -const forumConfig = config.data.roblox?.forumWatcher; -const logger = lily.child("forumWatcher"); - -export function isNewPost(post: Post): boolean { - const postId = post.id.toString(); - let isNew = true; - - isNew = !seenPosts.has(postId); - if (isNew) seenPosts.add(postId); - - if (new Date(post.createdAt).getTime() < startTime) isNew = false; - return isNew; -} - -async function fetchPosts(channel: { - channelId: string; - channelName: string; -}): Promise<Post[]> { - if (!forumConfig) - throw new Error("the forum watcher is not enabled in the configuration."); - - const response = await fetch( - `https://groups.roblox.com/v1/groups/${forumConfig.groupId}/forums/${channel.channelId}/posts`, - { - headers: { - Cookie: config.data.roblox?.cookie || "", - }, - }, - ); - - if (!response.ok) { - logger.error( - `failed to fetch posts: ${response.status} ${response.statusText}`, - ); - throw new Error("failed to fetch posts from the forum."); - } - - const data = await response.json(); - if (!data || !Array.isArray(data.data)) { - logger.error("invalid response format from the forum API."); - throw new Error("invalid response format from the forum API."); - } - - return data.data as Post[]; -} - -function inlineLink(text: string, link: string): string { - return `[${text}](${link})`; -} - -export async function uploadThumbnailAsEmoji( - client: Client | null, - imageUrl: string, - name: string, -) { - const rest = new REST({ version: "10" }).setToken(config.data.discord.token); - const imageResponse = await fetch(imageUrl); - if (!imageResponse.ok) { - logger.error(`failed to fetch image from ${imageUrl}`); - return null; - } - - const appId = config.data.discord.app_id; - - const imageBuffer = await imageResponse.arrayBuffer(); - - if (imageBuffer.byteLength > 256 * 1024) { - logger.warn("avatar image too large, skipping"); - return null; - } - - const base64Image = Buffer.from(imageBuffer).toString("base64"); - const imageData = `data:image/png;base64,${base64Image}`; - - try { - const randomId = Math.floor(Math.random() * 1000000); - client; - const emoji = await rest.post(Routes.applicationEmojis(appId), { - body: { - name: `${name}_avatar_${randomId}`, - image: imageData, - }, - }); - - return emoji as { id: string; name: string }; - } catch (_err) { - logger.error(`failed to upload emoji for ${name}: ${_err}`); - if (_err instanceof Error) { - logger.error(`error details: ${_err.message}`); - } else logger.error("unknown error occurred while uploading emoji."); - } - - return null; -} - -export async function watchForum(client: Client) { - if (!forumConfig || !forumConfig.enabled) return; - const channelId = forumConfig.notificationChannelId; - const channel = client.channels.cache.get(channelId) as GuildTextBasedChannel; - const forumChannels = forumConfig.channels; - - if (!channel || !channel.isTextBased()) { - logger.error( - `notification channel with ID ${channelId} is not a valid text channel.`, - ); - return; - } - - for (const forumChannel of forumChannels) { - try { - const posts = await fetchPosts(forumChannel); - const newPosts = posts.filter(isNewPost); - - const flaggedWords = [ - "ban", - "appeal", - "hacker", - "exploit", - "cheater", - "unban", - "moderator", - "admin", - "mod", - "exploiters", - "exploits", - ]; - - for (const post of newPosts) { - if (!post.firstComment || !post.firstComment.content) continue; - const foundWords = flaggedWords.filter( - (word) => - post.name.toLowerCase().includes(word) || - post.firstComment.content.plainText.toLowerCase().includes(word), - ); - const flagged = foundWords.length !== 0; - - const postLink = `https://roblox.com/communities/${forumConfig.groupId}/${forumConfig.groupName}#!/forums/${forumChannel.channelId}/post/${post.id}`; - const postContainer = new ContainerBuilder(); - - const { user, thumbnail } = await getRobloxUser( - String(post.createdBy), - 48, - ); - if ("code" in user) { - logger.error( - `failed to fetch user ${post.createdBy} for post ${post.id}: ${user.message}`, - ); - continue; - } - - let emoji: { id: string; name: string } | null = null; - - { - if (thumbnail.done) { - emoji = await uploadThumbnailAsEmoji( - client, - thumbnail.response.imageUri, - user.displayName || user.name, - ); - } - - const postTitle = new TextDisplayBuilder().setContent( - `## ${post.name}`, - ); - const postDescription = new TextDisplayBuilder().setContent( - post.firstComment.content.plainText || "no description provided", - ); - const postAuthor = new TextDisplayBuilder().setContent( - [ - emoji ? `<:${emoji.name}:${emoji.id}>` : "", - `**${inlineLink(user.displayName || user.name, `https://www.roblox.com/users/${user.id}/profile`)}**`, - ].join(" "), - ); - - const footerParts = [ - `user id: ${inlineCode(String(post.createdBy))}`, - `posted <t:${Math.round(new Date(post.createdAt).getTime() / 1000)}:R>`, - flagged - ? `matched words: ${foundWords.map((word) => inlineCode(word)).join(", ")}` - : undefined, - ].filter(Boolean); - const postFooter = new TextDisplayBuilder().setContent( - `-# ${footerParts.join(" • ")}`, - ); - - postContainer.addTextDisplayComponents( - postAuthor, - postTitle, - postDescription, - postFooter, - ); - } - - const bansContainer = await buildBansContainer(user); - const buttonRow = new ActionRowBuilder<ButtonBuilder>(); - - if (flagged) - buttonRow.addComponents( - new ButtonBuilder() - .setLabel("🚩") - .setCustomId("flagged_post") - .setStyle(ButtonStyle.Danger) - .setDisabled(true), - ); - - buttonRow.addComponents( - new ButtonBuilder() - .setLabel(forumChannel.channelName) - .setCustomId("forum_channel") - .setStyle(ButtonStyle.Primary) - .setDisabled(true), - new ButtonBuilder() - .setLabel("view post") - .setStyle(ButtonStyle.Link) - .setURL(postLink), - ); - - await channel.send({ - flags: ["IsComponentsV2"], - components: [postContainer, bansContainer, buttonRow], - }); - } - } catch (error) { - logger.error(`error while fetching forum posts: ${error}`); - } - } -}
M src/utils/logging.tssrc/utils/logging.ts

@@ -1,10 +1,15 @@

-import { Logger as Lily } from "@sillowww/lily"; -const lily = new Lily("bot"); +import { pino } from "pino"; -const loggers = { - events: lily.child("Events"), - interactions: lily.child("Interactions"), +const logger = pino({ + transport: { + target: "pino-pretty", + }, +}); + +export const loggers = { + events: logger.child({ name: "events" }), + interactions: logger.child({ name: "interactions" }), + config: logger.child({ name: "config" }), }; -export default lily; -export { loggers }; +export default logger;
M src/utils/permissions.tssrc/utils/permissions.ts

@@ -1,5 +1,6 @@

+import { db, guild, manager } from "@/database"; import type { GuildMember } from "discord.js"; -import { GuildModel } from "../database/schemas.ts"; +import { getGuild, getManagerRoleIds } from "./queries"; export async function hasManagerPermissions( member: GuildMember,

@@ -7,19 +8,12 @@ ): Promise<boolean> {

if ( member.permissions.has("ManageGuild") || member.guild.ownerId === member.id - ) { + ) return true; - } - - const guild = await GuildModel.findOne({ - guild_id: member.guild.id, - }); - if (!guild || guild.manager_roles.length === 0) { - return false; - } + const guild = await getGuild(member.guild.id); + const managers = await getManagerRoleIds(guild.data.guildId); + if (!guild || !managers || managers.length === 0) return false; - return member.roles.cache.some((role) => - guild.manager_roles.includes(role.id), - ); + return member.roles.cache.some((role) => managers.includes(role.id)); }
A src/utils/queries.ts

@@ -0,0 +1,113 @@

+import { + type Guild, + type Ticket, + db, + guild, + manager, + tickets, +} from "@/database"; +import type { Snowflake } from "discord.js"; +import { and, eq, sql } from "drizzle-orm"; + +type QueryResult<T, WillCreate extends boolean = false> = + | { exists: true; data: T } + | (WillCreate extends true + ? { exists: false; data: T } + : { exists: false; data: T | null }); + +export async function getGuild( + guildId: Snowflake, +): Promise<QueryResult<Guild, true>> { + const result = await db + .select() + .from(guild) + .where(eq(guild.guildId, guildId)) + .execute(); + + if (!result || result.length === 0) { + const newGuild = await db + .insert(guild) + .values({ guildId }) + .returning() + .execute(); + return { + exists: false, + data: newGuild[0], + }; + } + + return { + exists: true, + data: result[0], + }; +} + +export async function getTicketMessage( + guildId: Snowflake, +): Promise<QueryResult<string, false>> { + const guild = await getGuild(guildId); + if (!guild || !guild.data.ticket_message) { + return { + exists: false, + data: null, + }; + } + + return { + exists: true, + data: guild.data.ticket_message, + }; +} + +export async function getTicket( + guildId: Snowflake, + channelId: Snowflake, +): Promise<QueryResult<Ticket, false>> { + const guild = await getGuild(guildId); + if (!guild || !guild.data.ticket_message) { + return { + exists: false, + data: null, + }; + } + + const ticket = await db + .selectDistinct() + .from(tickets) + .where(and(eq(tickets.guildId, guildId), eq(tickets.channelId, channelId))) + .execute(); + + if (!ticket || ticket.length === 0) { + return { + exists: false, + data: null, + }; + } + + return { + exists: true, + data: ticket[0], + }; +} + +export async function getManagerRoleIds( + guildId: Snowflake, +): Promise<Snowflake[]> { + const rows = await db + .select({ roleId: manager.roleId }) + .from(manager) + .where(eq(manager.guildId, guildId)) + .execute(); + + return rows.map((r) => r.roleId); +} + +export async function countTickets(guildId: Snowflake) { + const result = await db + .select({ count: sql<number>`COUNT(*)` }) + .from(tickets) + .where(eq(tickets.guildId, guildId)) + .execute(); + + return result[0].count; +}
D src/utils/roblox.ts

@@ -1,124 +0,0 @@

-import config from "@/config"; -import type { - AvatarSize, - BLApiResponse, - GetUserResponse, - RobloxSearchResponse, - RobloxSearchUser, - ThumbnailResponse, -} from "@/types/roblox"; - -export async function getRobloxUser( - userId: string, - size: AvatarSize = 420, -): Promise<{ - user: GetUserResponse; - thumbnail: ThumbnailResponse; -}> { - if (!config.data.roblox) throw new Error("roblox API key is not configured"); - - const getUserUrl = `https://apis.roblox.com/cloud/v2/users/${userId}`; - const getThumbnailUrl = `https://apis.roblox.com/cloud/v2/users/${userId}:generateThumbnail?size=${size}`; - const [userRes, thumbnailRes] = await Promise.all([ - fetch(getUserUrl, { - headers: { - "Content-Type": "application/json", - "x-api-key": config.data.roblox.apiKey, - }, - }), - fetch(getThumbnailUrl, { - headers: { - "Content-Type": "application/json", - "x-api-key": config.data.roblox.apiKey, - }, - }), - ]); - - const [userData, thumbnailData] = await Promise.all([ - userRes.json(), - thumbnailRes.json(), - ]); - - return { user: userData, thumbnail: thumbnailData }; -} - -export async function getConnectedRobloxUser( - discordId: string, -): Promise<BLApiResponse> { - if (!config.data.bloxlink) - throw new Error("bloxlink token is not configured"); - - const response = await fetch( - `https://api.blox.link/v4/public/discord-to-roblox/${discordId}`, - { - headers: { - Authorization: config.data.bloxlink.token, - }, - }, - ); - - return await response.json(); -} - -export async function getRobloxIdFromUsername( - username: string, -): Promise<string | null> { - try { - const response = await fetch( - "https://users.roblox.com/v1/usernames/users", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - usernames: [username], - excludeBannedUsers: true, - }), - }, - ); - - if (!response.ok) { - return null; - } - - const data = await response.json(); - - if (data.data && data.data.length > 0 && data.data[0].id) { - return data.data[0].id.toString(); - } - - return null; - } catch (error) { - return null; - } -} - -export async function searchRobloxUsers( - keyword: string, - limit = 10, -): Promise<{ users: RobloxSearchUser[]; error?: string; code?: number }> { - try { - const response = await fetch( - `https://users.roblox.com/v1/users/search?keyword=${encodeURIComponent(keyword)}&limit=${limit}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - ...(config.data.roblox?.cookie - ? { Cookie: config.data.roblox.cookie } - : {}), - }, - }, - ); - - if (!response.ok) { - return { users: [], error: response.statusText, code: response.status }; - } - - const data: RobloxSearchResponse = await response.json(); - return { users: data.data || [] }; - } catch (error) { - return { users: [], error: "failed to search users" }; - } -}
A src/utils/s3.ts

@@ -0,0 +1,18 @@

+import config from "@/config"; +import { S3Client } from "bun"; + +export const s3 = new S3Client({ + accessKeyId: config.data.s3.accessKeyId, + secretAccessKey: config.data.s3.secretAccessKey, + bucket: config.data.s3.bucket, + endpoint: config.data.s3.endpoint, + region: config.data.s3.region, +}); + +export function getPublicUrl(key: string) { + if (config.data.s3.publicUrl) { + return `${config.data.s3.publicUrl}/${key}`; + } + + return s3.file(key).presign({ expiresIn: 3600 }); +}
D src/utils/userInfo.ts

@@ -1,169 +0,0 @@

-import type { GetUserOkResponse, GetUserResponse } from "@/types/roblox"; -import { - getConnectedRobloxUser, - getRobloxIdFromUsername, - getRobloxUser, -} from "@/utils/roblox"; -import { - type APIGuildMember, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - ContainerBuilder, - EmbedBuilder, - type GuildMember, - TextDisplayBuilder, -} from "discord.js"; -import { buildBansContainer } from "./bans"; -import { uploadThumbnailAsEmoji } from "./forumWatcher"; -import { hasManagerPermissions } from "./permissions"; - -export type AvatarSize = - | 48 - | 50 - | 60 - | 75 - | 100 - | 110 - | 150 - | 180 - | 352 - | 420 - | 720; - -export type UserInfoResult = { - user: GetUserResponse; - embed: EmbedBuilder; - containers: ContainerBuilder[]; - actionRows: ActionRowBuilder<ButtonBuilder>[]; -}; - -export async function getUserInfoFromRobloxUsername( - username: string, - member: GuildMember | APIGuildMember, -): Promise<UserInfoResult | { error: string }> { - const robloxId = await getRobloxIdFromUsername(username); - - if (!robloxId) { - return { error: "user not found or username is invalid" }; - } - - return getUserInfoFromRoblox(robloxId, member); -} - -export async function getUserInfoFromDiscord( - discordId: string, - member: GuildMember | APIGuildMember, -): Promise<UserInfoResult | { error: string }> { - const connectedAccount = await getConnectedRobloxUser(discordId); - if ("error" in connectedAccount) { - return { error: connectedAccount.error }; - } - - return getUserInfoFromRoblox(connectedAccount.robloxID, member); -} - -export async function getUserInfoFromRoblox( - robloxId: string, - member: GuildMember | APIGuildMember, - avatarSize: AvatarSize = 420, -): Promise<UserInfoResult | { error: string }> { - try { - const { user, thumbnail } = await getRobloxUser(robloxId, avatarSize); - if ("code" in user) - return { error: user.message || "failed to fetch user data" }; - - return formatUserInfo( - user, - thumbnail.done ? thumbnail.response.imageUri : null, - member, - ); - } catch (error) { - return { - error: - error instanceof Error ? error.message : "failed to fetch user data", - }; - } -} - -export async function formatUserInfo( - user: GetUserOkResponse, - thumbnailUrl: string | null, - member: GuildMember | APIGuildMember, -): Promise<UserInfoResult> { - const createdAt = new Date(user.createTime); - const timestamp = Math.floor(createdAt.getTime() / 1000); - const hasManagerPerms = await hasManagerPermissions(member); - - const thumbnailEmoji = await uploadThumbnailAsEmoji( - null, - thumbnailUrl || "", - user.displayName || user.name, - ); - - const userContainer = new ContainerBuilder(); - { - const emojiText = thumbnailEmoji - ? `<:${thumbnailEmoji.name}:${thumbnailEmoji.id}> ` - : ""; - const userTitle = new TextDisplayBuilder().setContent( - `## ${emojiText} ${user.displayName || user.name}`, - ); - const userDescription = new TextDisplayBuilder().setContent( - user.about || "no description provided", - ); - const userId = new TextDisplayBuilder().setContent(`**id:** ${user.id}`); - const userUsername = new TextDisplayBuilder().setContent( - `**username:** ${user.name}`, - ); - const userCreated = new TextDisplayBuilder().setContent( - `**created:** <t:${timestamp}> (<t:${timestamp}:R>)`, - ); - - userContainer.addTextDisplayComponents( - userTitle, - userDescription, - userId, - userUsername, - userCreated, - ); - } - - const embed = new EmbedBuilder() - .setAuthor({ - name: user.displayName || user.name, - url: `https://www.roblox.com/users/${user.id}/profile`, - }) - .setDescription(user.about || null) - .addFields( - { - name: "id", - value: user.id, - }, - { - name: "username", - value: user.name, - }, - { - name: "created", - value: `<t:${timestamp}> (<t:${timestamp}:R>)`, - }, - ) - .setColor(0x00aaff); - if (thumbnailUrl) embed.setThumbnail(thumbnailUrl); - const bansComponent = await buildBansContainer(user); - - return { - user: user, - embed: embed, - containers: [userContainer, ...(hasManagerPerms ? [bansComponent] : [])], - actionRows: [ - new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder() - .setURL(`https://www.roblox.com/users/${user.id}/profile`) - .setLabel("view profile") - .setStyle(ButtonStyle.Link), - ), - ], - }; -}