all repos — stealth-developers @ 117160d91a01e5457170e0b37df6d38c1a073119

feat(automod): perceptual hashes
vi did:web:vt3e.cat
Sat, 09 May 2026 03:17:39 +0100
commit

117160d91a01e5457170e0b37df6d38c1a073119

parent

ec1288b7069276a5ae7a7d415a9e456e7e9d3364

51 files changed, 1464 insertions(+), 384 deletions(-)

jump to
M bun.lockbun.lock

@@ -10,6 +10,8 @@ "discord-api-types": "^0.38.47",

"discord.js": "^14.26.2", "drizzle-orm": "^1.0.0-beta.9-e89174b", "fastest-levenshtein": "^1.0.16", + "imghash": "^1.1.4", + "jimp": "^1.6.1", "nanoid": "^5.1.7", "pino": "^10.3.1", "pino-pretty": "^13.1.3",

@@ -80,6 +82,22 @@ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],

"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + + "@canvas/image": ["@canvas/image@2.0.0", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "@cwasm/jpeg-turbo": "^0.1.1", "@cwasm/lodepng": "^0.1.2", "@cwasm/nsbmp": "^0.1.0", "@cwasm/nsgif": "^0.1.0", "@cwasm/webp": "^0.1.3", "decode-ico": "^0.4.1", "fast-base64-decode": "^1.0.0", "fast-base64-encode": "^1.0.0", "fast-base64-length": "^1.0.0", "simple-get": "^4.0.1" } }, "sha512-DQKEftZ5M4eM8Rzhv8FFZGdOsO8bVjzPC/9pBF+P//HjQM/Xf+PvQRTv97mVpsHt+Pejs1V7ooK6xf+dYgDNpw=="], + + "@canvas/image-data": ["@canvas/image-data@1.1.0", "", {}, "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA=="], + + "@cwasm/jpeg-turbo": ["@cwasm/jpeg-turbo@0.1.3", "", { "dependencies": { "@canvas/image-data": "^1.0.0" } }, "sha512-FkZxwwC6r4zhzlqM0nYGaMj/MDSrZPxLOdPdM6ySlgsMfOpNAZcLQkpNF4jP+DmsuUvRoeUD0YSMBvg3jYfK6w=="], + + "@cwasm/lodepng": ["@cwasm/lodepng@0.1.9", "", { "dependencies": { "@canvas/image-data": "^1.0.0" } }, "sha512-vb2H7/jTxnqJi7hHiEgtFm3smIqVpeY417vN+8cwsq3iTNrHzwnMzFXbeCf2H9Dl13Vh64qgUUcC2mxf6TPODA=="], + + "@cwasm/nsbmp": ["@cwasm/nsbmp@0.1.3", "", { "dependencies": { "@canvas/image-data": "^1.0.0" } }, "sha512-APiz9Rj2E049rBapTtwnCGqQeqJjmC85busDQ44UCaujU+LoggUm06NuS1WIBAZcDMRbJOgCFcRWc0tMT2kpfg=="], + + "@cwasm/nsgif": ["@cwasm/nsgif@0.1.2", "", { "dependencies": { "@canvas/image-data": "^1.0.0" } }, "sha512-LOD5HlL0O5jpnIAl+dLSZcB3v0RBNBjtoaymdCEPe2kyKzaP20BF+jy/QUyOZogQsgMVjusZES3tgwwoiiJ2rA=="], + + "@cwasm/webp": ["@cwasm/webp@0.1.5", "", { "dependencies": { "@canvas/image-data": "^1.0.0" } }, "sha512-ceIZQkyxK+s7mmItNcWqqHdOBiJAxYxTnrnPNgUNjldB1M9j+Bp/3eVIVwC8rUFyN/zoFwuT0331pyY3ackaNA=="], + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],

@@ -146,6 +164,62 @@ "@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=="], + "@jimp/core": ["@jimp/core@1.6.1", "", { "dependencies": { "@jimp/file-ops": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^21.3.3", "mime": "3" } }, "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A=="], + + "@jimp/diff": ["@jimp/diff@1.6.1", "", { "dependencies": { "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" } }, "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ=="], + + "@jimp/file-ops": ["@jimp/file-ops@1.6.1", "", {}, "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w=="], + + "@jimp/js-bmp": ["@jimp/js-bmp@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "bmp-ts": "^1.0.9" } }, "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ=="], + + "@jimp/js-gif": ["@jimp/js-gif@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w=="], + + "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "jpeg-js": "^0.4.4" } }, "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg=="], + + "@jimp/js-png": ["@jimp/js-png@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "pngjs": "^7.0.0" } }, "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA=="], + + "@jimp/js-tiff": ["@jimp/js-tiff@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "utif2": "^4.1.0" } }, "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw=="], + + "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ=="], + + "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/utils": "1.6.1" } }, "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ=="], + + "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ=="], + + "@jimp/plugin-color": ["@jimp/plugin-color@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A=="], + + "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg=="], + + "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg=="], + + "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg=="], + + "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g=="], + + "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1" } }, "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew=="], + + "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA=="], + + "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA=="], + + "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/js-bmp": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/js-tiff": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "any-base": "^1.1.0" } }, "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A=="], + + "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A=="], + + "@jimp/plugin-print": ["@jimp/plugin-print@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/types": "1.6.1", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w=="], + + "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.1", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww=="], + + "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg=="], + + "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg=="], + + "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-hash": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q=="], + + "@jimp/types": ["@jimp/types@1.6.1", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w=="], + + "@jimp/utils": ["@jimp/utils@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "tinycolor2": "^1.6.0" } }, "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw=="], + "@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=="],

@@ -188,6 +262,10 @@ "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],

"@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], "@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="],

@@ -205,14 +283,22 @@

"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "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=="], + "blockhash-core": ["blockhash-core@0.1.0", "", {}, "sha512-Cv7BgBo0jjVPaeuel4cvxf9LqIGsYNIPz9DAGvvrF9LRlEq9Q3HXu+S8bklPCae0sCxAXic4HGMoImf3FeO3Nw=="], + "bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="], + + "bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],

@@ -234,6 +320,12 @@ "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],

"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decode-bmp": ["decode-bmp@0.2.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "to-data-view": "^1.1.0" } }, "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA=="], + + "decode-ico": ["decode-ico@0.4.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "decode-bmp": "^0.2.0", "to-data-view": "^1.1.0" } }, "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "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=="],

@@ -260,6 +352,14 @@ "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=="], + "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], + + "fast-base64-decode": ["fast-base64-decode@1.0.0", "", {}, "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q=="], + + "fast-base64-encode": ["fast-base64-encode@1.0.0", "", {}, "sha512-z2XCzVK4fde2cuTEHu2QGkLD6BPtJNKJPn0Z7oINvmhq/quUuIIVPYKUdN0gYeZqOyurjJjBH/bUzK5gafyHvw=="], + + "fast-base64-length": ["fast-base64-length@1.0.0", "", {}, "sha512-MV+/ioblHx6SMjc/1l4EAnRJyAku6+6DxZ6RW0FoFCF1Aol/Ldb6FqwE3Kn3Ju1aam2m1KCIVoCljhgcG+Umzg=="], + "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=="],

@@ -269,6 +369,8 @@

"fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "file-type": ["file-type@10.11.0", "", {}, "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],

@@ -276,6 +378,8 @@ "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=="], + "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], + "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=="],

@@ -288,6 +392,12 @@ "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="],

"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], + + "image-type": ["image-type@4.1.0", "", { "dependencies": { "file-type": "^10.10.0" } }, "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg=="], + + "imghash": ["imghash@1.1.4", "", { "dependencies": { "@canvas/image": "^2.0.0", "blockhash-core": "^0.1.0", "image-type": "^4.1.0", "jpeg-js": "^0.4.1" } }, "sha512-atRI6S7rwrdZlr8pBsuk0NfBfeuPzyna98ugz2Sodj1P8SOZJLVYuqWnUrUFg2YnFTxtdbdSq4Ucn1osLFQ+qA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],

@@ -298,7 +408,11 @@ "is-url": ["is-url@1.2.4", "", {}, "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="],

"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "jimp": ["jimp@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/diff": "1.6.1", "@jimp/js-bmp": "1.6.1", "@jimp/js-gif": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/js-tiff": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/plugin-blur": "1.6.1", "@jimp/plugin-circle": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-contain": "1.6.1", "@jimp/plugin-cover": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-displace": "1.6.1", "@jimp/plugin-dither": "1.6.1", "@jimp/plugin-fisheye": "1.6.1", "@jimp/plugin-flip": "1.6.1", "@jimp/plugin-hash": "1.6.1", "@jimp/plugin-mask": "1.6.1", "@jimp/plugin-print": "1.6.1", "@jimp/plugin-quantize": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/plugin-rotate": "1.6.1", "@jimp/plugin-threshold": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1" } }, "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="],

@@ -334,6 +448,10 @@ "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],

"magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

@@ -348,6 +466,8 @@ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],

"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=="], + "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], + "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=="],

@@ -356,6 +476,14 @@ "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=="],

"opencollective-postinstall": ["opencollective-postinstall@2.0.3", "", { "bin": { "opencollective-postinstall": "index.js" } }, "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="], + + "parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="], + + "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="], + "pino": ["pino@10.3.1", "", { "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-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],

@@ -363,6 +491,10 @@

"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=="], "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], + + "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], + + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],

@@ -392,10 +524,18 @@ "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],

"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + "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=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "simple-xml-to-json": ["simple-xml-to-json@1.2.7", "", {}, "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q=="], + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],

@@ -406,6 +546,8 @@ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],

"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], "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=="],

@@ -416,6 +558,12 @@ "tesseract.js-core": ["tesseract.js-core@7.0.0", "", {}, "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw=="],

"thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + + "to-data-view": ["to-data-view@1.1.0", "", {}, "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],

@@ -426,10 +574,14 @@ "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=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "uwuifier": ["uwuifier@4.2.2", "", {}, "sha512-Fhtj3Yg3rrDTEs+L1fqkgM3VYGJrP4U9GY+4fPdw08T5kzOB8NSosZnZfa5Zhv1Hh7NCpH5G6t1DIZyM1wPK1w=="],

@@ -448,6 +600,12 @@ "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=="],

"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], + + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "zlibjs": ["zlibjs@0.3.1", "", {}, "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],

@@ -472,9 +630,47 @@ "@discordjs/ws/@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],

"@discordjs/ws/discord-api-types": ["discord-api-types@0.38.31", "", {}, "sha512-kC94ANsk8ackj8ENTuO8joTNEL0KtymVhHy9dyEC/s4QAZ7GCx40dYEzQaadyo8w+oP0X8QydE/nzAWRylTGtQ=="], + "@jimp/core/file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], + + "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-color/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-contain/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-cover/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-crop/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-displace/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-fisheye/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-flip/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-mask/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-print/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-quantize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-resize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-rotate/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-threshold/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@libsql/hrana-client/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=="], "discord.js/discord-api-types": ["discord-api-types@0.38.44", "", {}, "sha512-q91MgBzP/gRaCLIbQTaOrOhbD8uVIaPKxpgX2sfFB2nZ9nSiTYM9P3NFQ7cbO6NCxctI6ODttc67MI+YhIfILg=="], + + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], + + "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], "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=="],
M frontend/index.htmlfrontend/index.html

@@ -1,16 +1,17 @@

<!doctype html> <html lang=""> - <head> - <meta charset="UTF-8" /> - <link rel="icon" href="/favicon.ico" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>Vite App</title> + <head> + <meta charset="UTF-8" /> + <link rel="icon" href="/favicon.ico" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>tickets</title> + <meta name="description" content="what dhe tuna iz dis ...ip graber" /> - <link rel="preconnect" href="https://fonts.googleapis.com" /> - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> - </head> - <body> - <div id="app"></div> - <script type="module" src="/src/main.ts"></script> - </body> + <link rel="preconnect" href="https://fonts.googleapis.com" /> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> + </head> + <body> + <div id="app"></div> + <script type="module" src="/src/main.ts"></script> + </body> </html>
M package.jsonpackage.json

@@ -21,6 +21,8 @@ "discord-api-types": "^0.38.47",

"discord.js": "^14.26.2", "drizzle-orm": "^1.0.0-beta.9-e89174b", "fastest-levenshtein": "^1.0.16", + "imghash": "^1.1.4", + "jimp": "^1.6.1", "nanoid": "^5.1.7", "pino": "^10.3.1", "pino-pretty": "^13.1.3",
M src/automod/index.tssrc/automod/index.ts

@@ -9,13 +9,12 @@ type Snowflake,

} from "discord.js"; import { distance } from "fastest-levenshtein"; import Tesseract from "tesseract.js"; -import { createHash } from "node:crypto"; +import imageHash from "imghash"; import { hasManagerPermissions } from "@/utils/discord/permissions"; import { loggers } from "@/utils/logging"; import { db } from "@/database"; import { automodHashes } from "@/database/schema/automod"; -import { eq } from "drizzle-orm"; import { isProd } from "@/config"; const logger = loggers.automod;

@@ -29,7 +28,9 @@ const pendingReports = new Map<Snowflake, Pending>();

const cooldowns = new Map<Snowflake, NodeJS.Timeout>(); async function getLogChannel(client: Client) { - return client.channels.fetch(isProd ? "1493015326554587146" : "1493015912234881205"); + return client.channels.fetch( + isProd ? "1493015326554587146" : "1493015912234881205", + ); } type AutomodResult = { delete: true } | null;

@@ -37,19 +38,28 @@ type AutomodSignals = {

messageCount?: number; lastMessage?: Message; }; -type AutomodFunction = (client: Client, message: Message) => Promise<AutomodResult>; +type AutomodFunction = ( + client: Client, + message: Message, +) => Promise<AutomodResult>; // oxlint-disable-next-line no-unused-vars -async function fetchSignals(client: Client, message: Message): Promise<AutomodSignals> { +async function fetchSignals( + client: Client, + message: Message, +): Promise<AutomodSignals> { const signals: AutomodSignals = { messageCount: undefined, }; - const messages = (await rest.get(`/guilds/${message.guildId}/messages/search`, { - body: { - author_id: [message.author.id], - } as RESTGetAPIGuildMessagesSearchQuery, - })) as RESTGetAPIGuildMessagesSearchResult; + const messages = (await rest.get( + `/guilds/${message.guildId}/messages/search`, + { + body: { + author_id: [message.author.id], + } as RESTGetAPIGuildMessagesSearchQuery, + }, + )) as RESTGetAPIGuildMessagesSearchResult; if ("total_results" in messages) { signals.messageCount = messages.total_results;

@@ -57,7 +67,9 @@ const [lastApiMessage] = messages.messages[1];

const channel = await client.channels.fetch(lastApiMessage.channel_id); if (channel?.isTextBased()) { - const lastMessage = await channel.messages.fetch(lastApiMessage.id).catch(() => null); + const lastMessage = await channel.messages + .fetch(lastApiMessage.id) + .catch(() => null); if (lastMessage) { signals.lastMessage = lastMessage; }

@@ -67,11 +79,32 @@

return signals; } +function hammingDistance(hex1: string, hex2: string): number { + let dist = 0; + const len = Math.min(hex1.length, hex2.length); + for (let i = 0; i < len; i++) { + const val1 = Number.parseInt(hex1[i], 16); + const val2 = Number.parseInt(hex2[i], 16); + let diff = val1 ^ val2; + while (diff > 0) { + dist += diff & 1; + diff >>= 1; + } + } + return dist; +} + export async function handleMessage(client: Client, message: Message) { if (message.author.bot || !message.guildId || !message.member) return; - const automodFunctions: AutomodFunction[] = [handleSpamMarker, handleCryptoScamOCR]; + const automodFunctions: AutomodFunction[] = [ + handleSpamMarker, + handleCryptoScamOCR, + ]; - if (message.channel.type == ChannelType.GuildText && message.channel.name.includes("ticket")) + if ( + message.channel.type == ChannelType.GuildText && + message.channel.name.includes("ticket") + ) return; if (message.channel.isThread()) return null; if (message.content.includes("!bypass")) return null;

@@ -109,7 +142,10 @@ )

.catch(() => null); } - const timer = setTimeout(() => flushReport(client, message.author.id), 30_000); + const timer = setTimeout( + () => flushReport(client, message.author.id), + 30_000, + ); pendingReports.set(message.author.id, { channels: new Set([message.channelId]), reasons: new Set([reason]),

@@ -120,14 +156,23 @@ existing.channels.add(message.channelId);

existing.reasons.add(reason); clearTimeout(existing.timer); - existing.timer = setTimeout(() => flushReport(client, message.author.id), 30_000); + existing.timer = setTimeout( + () => flushReport(client, message.author.id), + 30_000, + ); } } -async function handleCryptoScamOCR(client: Client, message: Message): Promise<AutomodResult> { +async function handleCryptoScamOCR( + client: Client, + message: Message, +): Promise<AutomodResult> { if (message.attachments.size === 0) return null; - if (pendingReports.has(message.author.id) || cooldowns.has(message.author.id)) { + if ( + pendingReports.has(message.author.id) || + cooldowns.has(message.author.id) + ) { logger.info( { messageId: message.id, authorId: message.author.id }, "Skipping OCR as user has already been recently flagged",

@@ -165,16 +210,27 @@ if (!imageResponse.ok) continue;

const arrayBuffer = await imageResponse.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); - const imageHash = createHash("sha256").update(buffer).digest("hex"); + + const pHash = await imageHash.hash(buffer); + + const existingHashes = await db.select().from(automodHashes); - const existingHash = await db - .select() - .from(automodHashes) - .where(eq(automodHashes.hash, imageHash)); + let matchedHash = false; + for (const existing of existingHashes) { + const dist = hammingDistance(pHash, existing.hash); + if (dist <= 12) { + matchedHash = true; + break; + } + } - if (existingHash) { + if (matchedHash) { logger.info( - { messageId: message.id, authorId: message.author.id, imageHash }, + { + messageId: message.id, + authorId: message.author.id, + imageHash: pHash, + }, "Image matches known scam hash in DB, skipping OCR", );

@@ -188,7 +244,10 @@

return { delete: true }; } - logger.debug({ messageId: message.id, imageHash }, "Running OCR on image attachment"); + logger.debug( + { messageId: message.id, imageHash: pHash }, + "Running OCR on image attachment", + ); const { data: { text },

@@ -202,9 +261,14 @@ .replace(/[^\w\s]/g, " ")

.replace(/\s+/g, " ") .trim(); - logger.debug({ messageId: message.id, extractedText: normalizedText }, "OCR completed"); + logger.debug( + { messageId: message.id, extractedText: normalizedText }, + "OCR completed", + ); - let isScam = scamPatterns.some((pattern) => normalizedText.includes(pattern)); + let isScam = scamPatterns.some((pattern) => + normalizedText.includes(pattern), + ); if (!isScam) { const words = normalizedText.split(" ");

@@ -216,7 +280,9 @@

for (let i = 0; i <= words.length - windowSize; i++) { const window = words.slice(i, i + windowSize).join(" "); const similarity = - 1 - distance(pattern, window) / Math.max(pattern.length, window.length); + 1 - + distance(pattern, window) / + Math.max(pattern.length, window.length); if (similarity > 0.7) { logger.info(

@@ -265,12 +331,20 @@ try {

await db .insert(automodHashes) .values({ - hash: imageHash, + hash: pHash, addedAt: Date.now(), }) .onConflictDoNothing(); + + logger.debug( + { messageId: message.id, imageHash: pHash }, + "saved scam image hash to DB", + ); } catch (err) { - logger.error({ err, imageHash }, "failed to save scam image hash to DB"); + logger.error( + { err, imageHash: pHash }, + "failed to save scam image hash to DB", + ); } }

@@ -296,14 +370,20 @@

return { delete: true }; } } catch (err) { - logger.error({ err, messageId: message.id }, "failed to process image for ocr"); + logger.error( + { err, messageId: message.id }, + "failed to process image for ocr", + ); } } return null; } -async function handleSpamMarker(client: Client, message: Message): Promise<AutomodResult> { +async function handleSpamMarker( + client: Client, + message: Message, +): Promise<AutomodResult> { const { content } = message; /*

@@ -314,11 +394,19 @@ if a messsage contains a link marker, then we delete it.

if a message contains a word marker, we first check if the last message is older than an hour, if it is, we delete the message. */ - const linkMarkers = ["discord.gg/", "discordapp.com/invite/", "discord.com/invite/"]; + const linkMarkers = [ + "discord.gg/", + "discordapp.com/invite/", + "discord.com/invite/", + ]; const wordMarkers = ["check my bio"]; - const hasLinkMarker = linkMarkers.some((marker) => content.toLowerCase().includes(marker)); - const hasWordMarker = wordMarkers.some((marker) => content.toLowerCase().includes(marker)); + const hasLinkMarker = linkMarkers.some((marker) => + content.toLowerCase().includes(marker), + ); + const hasWordMarker = wordMarkers.some((marker) => + content.toLowerCase().includes(marker), + ); if (!hasLinkMarker && !hasWordMarker) return null; // const signals = await fetchSignals(client, message);

@@ -368,6 +456,9 @@ logChannel.send(

`<@${authorId}> posted flagged messages in channels: ${channelMentions}\n> ${reasons}`, ); - const cooldownTimer = setTimeout(() => cooldowns.delete(authorId), 5 * 60 * 1000); + const cooldownTimer = setTimeout( + () => cooldowns.delete(authorId), + 5 * 60 * 1000, + ); cooldowns.set(authorId, cooldownTimer); }
M src/config.tssrc/config.ts

@@ -59,12 +59,15 @@

function validateConfig() { const env = process.env.NODE_ENV || "DEV"; - const runtimeConfig = JSON.parse(fs.readFileSync(`.config.${env}.json`, "utf8")); + const runtimeConfig = JSON.parse( + fs.readFileSync(`.config.${env}.json`, "utf8"), + ); const conf = schema.safeParse(runtimeConfig); if (conf.success) return conf.data; logger.error("invalid environment variables"); - for (const err of conf.error.issues) logger.error(` ${err.message}: ${err.path}`); + for (const err of conf.error.issues) + logger.error(` ${err.message}: ${err.path}`); process.exit(1); }
M src/database/queries.tssrc/database/queries.ts

@@ -1,16 +1,35 @@

-import { type Guild, type Ticket, db, guild, manager, tickets } from "@/database"; +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 }); + | (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(); +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(); + const newGuild = await db + .insert(guild) + .values({ guildId }) + .returning() + .execute(); return { exists: false, data: newGuild[0],

@@ -23,7 +42,9 @@ data: result[0],

}; } -export async function getTicketMessage(guildId: Snowflake): Promise<QueryResult<string, false>> { +export async function getTicketMessage( + guildId: Snowflake, +): Promise<QueryResult<string, false>> { const guild = await getGuild(guildId); if (!guild || !guild.data.ticket_message) { return {

@@ -116,7 +137,9 @@

const ticket = await db .select() .from(tickets) - .where(and(eq(tickets.guildId, guildId), eq(tickets.ticketNumber, numericId))) + .where( + and(eq(tickets.guildId, guildId), eq(tickets.ticketNumber, numericId)), + ) .execute(); if (!ticket || ticket.length === 0) {

@@ -132,7 +155,9 @@ data: ticket[0],

}; } -export async function getManagerRoleIds(guildId: Snowflake): Promise<Snowflake[]> { +export async function getManagerRoleIds( + guildId: Snowflake, +): Promise<Snowflake[]> { const rows = await db .select({ roleId: manager.roleId }) .from(manager)
M src/database/schema/bugs.tssrc/database/schema/bugs.ts

@@ -16,7 +16,10 @@

title: text("title").notNull(), description: text("description").notNull(), - projects: text("projects", { mode: "json" }).$type<string[]>().default([]).notNull(), + projects: text("projects", { mode: "json" }) + .$type<string[]>() + .default([]) + .notNull(), sent: integer("sent", { mode: "boolean" }).default(false).notNull(), messageId: text("message_id"),
M src/database/schema/tickets.tssrc/database/schema/tickets.ts

@@ -15,7 +15,10 @@ anonymousId: text("anonymous_id").notNull(),

claimedBy: text("claimed_by"), claimedAt: integer("claimed_at", { mode: "timestamp" }), - addedUsers: text("added_users", { mode: "json" }).$type<string[]>().default([]).notNull(), + addedUsers: text("added_users", { mode: "json" }) + .$type<string[]>() + .default([]) + .notNull(), type: text("type").notNull(), status: text("status", { enum: ["open", "closed", "archived"] })
M src/events/interactionCreate.tssrc/events/interactionCreate.ts

@@ -15,7 +15,10 @@ if (!command || !command.execute) return;

try { await command.execute(client, interaction); } catch (error) { - logger.error(error, `there was an error while executing ${interaction.commandName}`); + logger.error( + error, + `there was an error while executing ${interaction.commandName}`, + ); if (interaction.deferred || interaction.replied) { await interaction.followUp({
M src/events/messageCreate.tssrc/events/messageCreate.ts

@@ -1,8 +1,20 @@

-import { AttachmentBuilder, type Client, Events, type GuildMember, type Message } from "discord.js"; +import { + AttachmentBuilder, + type Client, + Events, + type GuildMember, + type Message, +} from "discord.js"; import { and, eq } from "drizzle-orm"; import { nanoid } from "nanoid"; -import { attachments, db, ticketMessages, tickets, userPreferences } from "@/database"; +import { + attachments, + db, + ticketMessages, + tickets, + userPreferences, +} from "@/database"; import { getPublicUrl, s3 } from "@/utils/s3"; import { handleMessage } from "@/automod";

@@ -31,7 +43,9 @@ // const allowed = await checkUwuifyRateLimit(message.author.id);

const allowed = true; if (!allowed) { const info = await getRateLimitInfo(message.author.id); - const resetTime = info?.resetAt ? Math.floor(info.resetAt.getTime() / 1000) : Date.now(); + const resetTime = info?.resetAt + ? Math.floor(info.resetAt.getTime() / 1000) + : Date.now(); try { await message.reply({

@@ -52,9 +66,14 @@ try {

let replyReference = null; if (message.reference?.messageId) { try { - replyReference = await message.channel.messages.fetch(message.reference.messageId); + replyReference = await message.channel.messages.fetch( + message.reference.messageId, + ); } catch (error) { - logger.warn(error, "Failed to fetch referenced message for uwuified reply"); + logger.warn( + error, + "Failed to fetch referenced message for uwuified reply", + ); } }

@@ -63,7 +82,9 @@ for (const attachment of message.attachments.values()) {

try { const resp = await fetch(attachment.url); if (!resp.ok) { - logger.warn(`Failed to fetch attachment ${attachment.name}, status ${resp.status}`); + logger.warn( + `Failed to fetch attachment ${attachment.name}, status ${resp.status}`, + ); continue; }

@@ -79,7 +100,9 @@ }

message.delete(); - const contentParts = [`**<@${message.author.id}>**: ${uwuify(message.content)}`]; + const contentParts = [ + `**<@${message.author.id}>**: ${uwuify(message.content)}`, + ]; if (message.channel.isSendable()) { const sendOptions: {

@@ -124,7 +147,9 @@

const triggers = ["grok is this true", "grok is this false "]; if ( message.channel.isSendable() && - triggers.some((trigger) => message.content.toLowerCase().includes(trigger)) + triggers.some((trigger) => + message.content.toLowerCase().includes(trigger), + ) ) { const chance = Math.random() * 100; if (chance < 25 || message.author.id === "470956810866655242") {

@@ -135,7 +160,10 @@ message.channel.send("yeh");

} } - if (message.channel.isSendable() && message.content.includes("gorque is this true")) { + if ( + message.channel.isSendable() && + message.content.includes("gorque is this true") + ) { message.channel.send("oui 🇫🇷🥖"); }

@@ -145,7 +173,10 @@ const ticket = db

.select() .from(tickets) .where( - and(eq(tickets.channelId, message.channelId), eq(tickets.guildId, message.guildId ?? "")), + and( + eq(tickets.channelId, message.channelId), + eq(tickets.guildId, message.guildId ?? ""), + ), ) .get();

@@ -153,10 +184,15 @@ if (!ticket) return;

try { const isSystem = message.author.id === client.user.id; - const isModerator = await hasManagerPermissions(message?.member as GuildMember); + const isModerator = await hasManagerPermissions( + message?.member as GuildMember, + ); const authorType = isSystem ? "system" : isModerator ? "staff" : "user"; - let anonymisedContent = message.content.replace(new RegExp(ticket.authorId, "g"), "Reporter"); + let anonymisedContent = message.content.replace( + new RegExp(ticket.authorId, "g"), + "Reporter", + ); for (const [index, userId] of (ticket.addedUsers || []).entries()) { anonymisedContent = anonymisedContent.replace(

@@ -193,7 +229,8 @@

const formatSize = (size: number) => { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`; - if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(2)} MB`; + if (size < 1024 * 1024 * 1024) + return `${(size / (1024 * 1024)).toFixed(2)} MB`; return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`; };

@@ -236,7 +273,9 @@ if (!statusMessage) continue;

try { statusList[i].status = "uploading"; - await statusMessage.edit(`**Attachment upload status**\n${renderBoard()}`); + await statusMessage.edit( + `**Attachment upload status**\n${renderBoard()}`, + ); const key = `${ticket.anonymousId}/${savedMessage.id}/${nanoid(8)}_${attachment.name}`; const file = s3.file(key);

@@ -261,11 +300,18 @@ });

statusList[i].status = "done"; statusList[i].url = publicUrl; - await statusMessage.edit(`**Attachment upload status**\n${renderBoard()}`); + await statusMessage.edit( + `**Attachment upload status**\n${renderBoard()}`, + ); } catch (error) { - logger.error(error, `Failed to upload attachment ${attachment.name}`); + logger.error( + error, + `Failed to upload attachment ${attachment.name}`, + ); statusList[i].status = "failed"; - await statusMessage.edit(`**Attachment upload status**\n${renderBoard()}`); + await statusMessage.edit( + `**Attachment upload status**\n${renderBoard()}`, + ); } } }
M src/events/messageDelete.tssrc/events/messageDelete.ts

@@ -14,7 +14,10 @@ const ticket = db

.select() .from(tickets) .where( - and(eq(tickets.channelId, message.channelId), eq(tickets.guildId, message.guildId ?? "")), + and( + eq(tickets.channelId, message.channelId), + eq(tickets.guildId, message.guildId ?? ""), + ), ) .get();
M src/handlers/events.tssrc/handlers/events.ts

@@ -25,7 +25,10 @@ logger.info(`found ${events.size} events.`);

for (const event of events.values()) { logger.info(`registering event: ${event.event}`); - client[event.once ? "once" : "on"](event.event, event.execute.bind(null, client)); + client[event.once ? "once" : "on"]( + event.event, + event.execute.bind(null, client), + ); } logger.info(`registered ${events.size} events`);
M src/handlers/interactions.tssrc/handlers/interactions.ts

@@ -14,7 +14,11 @@ const processFile = async (fileUrl: string) => {

const { default: interaction } = await import(fileUrl); if (!interaction) return; - if (!interaction.data && !interaction.buttonExecute && !interaction.modalExecute) { + if ( + !interaction.data && + !interaction.buttonExecute && + !interaction.modalExecute + ) { logger.info(`${fileUrl} does not have a data property, skipping`); return; }

@@ -37,12 +41,19 @@ commands.set(commandName, interaction);

return interaction; }; - return await crawlDirectory<ICommand>(getHandlerPath("interactions"), processFile); + return await crawlDirectory<ICommand>( + getHandlerPath("interactions"), + processFile, + ); } -async function registerInteractions(client: Client, interactions: ApplicationCommandData[]) { +async function registerInteractions( + client: Client, + interactions: ApplicationCommandData[], +) { if (!client.user) return logger.error("client user is not available"); - if (!client.application) return logger.error("client application is not available"); + if (!client.application) + return logger.error("client application is not available"); if (interactions.length === 0) return; logger.info("registering interactions...");
M src/interactions/buttons/transcript-btn.tssrc/interactions/buttons/transcript-btn.ts

@@ -19,7 +19,10 @@ });

return; } - if (interaction.member && (await hasManagerPermissions(interaction.member as GuildMember))) { + if ( + interaction.member && + (await hasManagerPermissions(interaction.member as GuildMember)) + ) { await db.insert(ticketAccessLogs).values({ ticketId: ticket.id, userId: interaction.user.id,
M src/interactions/commands/bug/_shared.tssrc/interactions/commands/bug/_shared.ts

@@ -101,7 +101,8 @@ }

export function buildReportModal(bugId?: string, bug?: Bug) { const terminology = config.terminology; - const capitalizedTerminology = terminology.charAt(0).toUpperCase() + terminology.slice(1); + const capitalizedTerminology = + terminology.charAt(0).toUpperCase() + terminology.slice(1); const projectOptions = bug ? getProjectChoices("label", bug.projects[0])

@@ -139,7 +140,9 @@ );

const descriptionLabel = new LabelBuilder() .setLabel("Bug Description") - .setDescription(bug ? "Update the bug description" : "Describe the bug in detail") + .setDescription( + bug ? "Update the bug description" : "Describe the bug in detail", + ) .setTextInputComponent( (() => { const input = new TextInputBuilder()

@@ -161,11 +164,18 @@ modal.setLabelComponents(gameLabel, titleLabel, descriptionLabel);

} else { const mediaLabel = new LabelBuilder() .setLabel("Media") - .setDescription("Provide any screenshots or videos that demonstrate the bug") + .setDescription( + "Provide any screenshots or videos that demonstrate the bug", + ) .setFileUploadComponent( new FileUploadBuilder().setCustomId(INPUT_IDS.MEDIA).setRequired(false), ); - modal.setLabelComponents(gameLabel, titleLabel, descriptionLabel, mediaLabel); + modal.setLabelComponents( + gameLabel, + titleLabel, + descriptionLabel, + mediaLabel, + ); } return modal;
M src/interactions/commands/bug/buttons.tssrc/interactions/commands/bug/buttons.ts

@@ -14,7 +14,10 @@ const results = await db.select().from(bugs).where(eq(bugs.id, parsedId));

return results[0] ?? null; } -export async function handleCloseButton(client: Client, interaction: ButtonInteraction) { +export async function handleCloseButton( + client: Client, + interaction: ButtonInteraction, +) { const guildId = interaction.guildId; if (!guildId) return;

@@ -53,7 +56,12 @@ return;

} if (bug.messageId) { - await updateBugEmbed(client, { ...bug, status: "closed" }, bug.messageId, channelId); + await updateBugEmbed( + client, + { ...bug, status: "closed" }, + bug.messageId, + channelId, + ); } await interaction.reply({

@@ -62,7 +70,10 @@ flags: ["Ephemeral"],

}); } -export async function handleOpenButton(client: Client, interaction: ButtonInteraction) { +export async function handleOpenButton( + client: Client, + interaction: ButtonInteraction, +) { const guildId = interaction.guildId; if (!guildId) return;

@@ -102,7 +113,12 @@ return;

} if (bug.messageId) { - await updateBugEmbed(client, { ...bug, status: "open" }, bug.messageId, channelId); + await updateBugEmbed( + client, + { ...bug, status: "open" }, + bug.messageId, + channelId, + ); } await interaction.reply({

@@ -111,7 +127,10 @@ flags: ["Ephemeral"],

}); } -export async function handleEditButton(_client: Client, interaction: ButtonInteraction) { +export async function handleEditButton( + _client: Client, + interaction: ButtonInteraction, +) { const { parts } = parseComponentId(interaction.customId); const id = parts[1]; const bug = await fetchBugById(id);

@@ -128,7 +147,10 @@ const modal = buildReportModal(String(bug.id), bug);

await interaction.showModal(modal); } -export async function handleDeleteButton(_client: Client, interaction: ButtonInteraction) { +export async function handleDeleteButton( + _client: Client, + interaction: ButtonInteraction, +) { const guildId = interaction.guildId; if (!guildId) return;

@@ -143,7 +165,9 @@ });

return; } - const isManager = await hasManagerPermissions(interaction.member as GuildMember); + const isManager = await hasManagerPermissions( + interaction.member as GuildMember, + ); const isBugReporter = bug.authorId === interaction.user.id; if (!isManager && !isBugReporter) {

@@ -166,7 +190,8 @@ }

const channelId = data.bug_channel_id; if (!channelId) { await interaction.reply({ - content: "✅ Bug removed from database. (No bug channel configured to remove message/thread)", + content: + "✅ Bug removed from database. (No bug channel configured to remove message/thread)", flags: ["Ephemeral"], }); return;

@@ -195,7 +220,10 @@ flags: ["Ephemeral"],

}); } -export async function handleTrelloButton(_client: Client, interaction: ButtonInteraction) { +export async function handleTrelloButton( + _client: Client, + interaction: ButtonInteraction, +) { const { parts } = parseComponentId(interaction.customId); const id = parts[1]; const bug = await fetchBugById(id);
M src/interactions/commands/bug/index.tssrc/interactions/commands/bug/index.ts

@@ -21,17 +21,25 @@

const commandData = new SlashCommandBuilder() .setName("bug") .setDescription("Make a bug report") - .addSubcommand((subcommand) => subcommand.setName("report").setDescription("Report a new bug")) + .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(false), + option + .setName("user") + .setDescription("The user to send the button to") + .setRequired(false), ), ); -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { if (!interaction.isChatInputCommand()) return; const subcommand = interaction.options.getSubcommand();

@@ -104,7 +112,10 @@ });

} } -async function modalExecute(client: Client, interaction: ModalSubmitInteraction) { +async function modalExecute( + client: Client, + interaction: ModalSubmitInteraction, +) { const [, action] = interaction.customId.split(":"); switch (action) { case "edit":
M src/interactions/commands/bug/modals.tssrc/interactions/commands/bug/modals.ts

@@ -4,14 +4,24 @@ import { parseComponentId } from "@/utils/discord/components";

import { AttachmentBuilder } from "discord.js"; import type { Client, ModalSubmitInteraction } from "discord.js"; import { eq, sql } from "drizzle-orm"; -import { INPUT_IDS, VALIDATION, constructContainer, updateBugEmbed } from "./_shared"; +import { + INPUT_IDS, + VALIDATION, + constructContainer, + updateBugEmbed, +} from "./_shared"; -export async function handleNewBugModal(_client: Client, interaction: ModalSubmitInteraction) { +export async function handleNewBugModal( + _client: Client, + interaction: ModalSubmitInteraction, +) { const guildId = interaction.guildId; if (!guildId) return; const title = interaction.fields.getTextInputValue(INPUT_IDS.TITLE); - const description = interaction.fields.getTextInputValue(INPUT_IDS.DESCRIPTION); + const description = interaction.fields.getTextInputValue( + INPUT_IDS.DESCRIPTION, + ); const affected = interaction.fields.getStringSelectValues(INPUT_IDS.AFFECTED); const media = interaction.fields.getUploadedFiles(INPUT_IDS.MEDIA); await interaction.deferReply({ flags: ["Ephemeral"] });

@@ -68,7 +78,11 @@ console.error("error processing uploaded file:", err);

} } - const container = await constructContainer(bug[0], interaction.user.id, attachment); + const container = await constructContainer( + bug[0], + interaction.user.id, + attachment, + ); if (!container) return; const { data, exists } = await getGuild(guildId);

@@ -88,7 +102,9 @@ });

// thread function truncateTitle(title: string, maxLength: number): string { - return title.length > maxLength ? `${title.substring(0, maxLength)}...` : title; + return title.length > maxLength + ? `${title.substring(0, maxLength)}...` + : title; } const paddedNumber = bug[0].bugNumber.toString().padStart(4, "0"); const threadName = `#${paddedNumber} ${truncateTitle(title, VALIDATION.THREAD_NAME_MAX_LENGTH)}`;

@@ -103,11 +119,18 @@ .update(bugs)

.set({ messageId: message.id, threadId: thread.id, sent: true }) .where(eq(bugs.id, bug[0].id)); - thread.send("Use this space to discuss the bug, provide additional details, or ask questions."); - interaction.editReply(`Bug #${paddedNumber} created successfully! Find it here: ${message.url}`); + thread.send( + "Use this space to discuss the bug, provide additional details, or ask questions.", + ); + interaction.editReply( + `Bug #${paddedNumber} created successfully! Find it here: ${message.url}`, + ); } -export async function handleEditModal(client: Client, interaction: ModalSubmitInteraction) { +export async function handleEditModal( + client: Client, + interaction: ModalSubmitInteraction, +) { const guildId = interaction.guildId; if (!guildId) return;

@@ -122,7 +145,9 @@ return;

} const title = interaction.fields.getTextInputValue(INPUT_IDS.TITLE); - const description = interaction.fields.getTextInputValue(INPUT_IDS.DESCRIPTION); + const description = interaction.fields.getTextInputValue( + INPUT_IDS.DESCRIPTION, + ); const affected = interaction.fields.getStringSelectValues(INPUT_IDS.AFFECTED); if (

@@ -167,7 +192,8 @@

const { data, exists } = await getGuild(guildId); if (!exists) { await interaction.reply({ - content: "✅ Bug updated, but guild configuration missing for updating the public message.", + content: + "✅ Bug updated, but guild configuration missing for updating the public message.", flags: ["Ephemeral"], }); return;

@@ -175,7 +201,8 @@ }

const channelId = data.bug_channel_id; if (!channelId) { await interaction.reply({ - content: "✅ Bug updated, but no bug channel configured to update message.", + content: + "✅ Bug updated, but no bug channel configured to update message.", flags: ["Ephemeral"], }); return;
M src/interactions/commands/codes.tssrc/interactions/commands/codes.ts

@@ -12,10 +12,16 @@ const commandData = new SlashCommandBuilder()

.setName("codes") .setDescription(`Get the codes for all ${config.terminology}s`) .addUserOption((option) => - option.setName("user").setDescription("Optional user to mention").setRequired(false), + option + .setName("user") + .setDescription("Optional user to mention") + .setRequired(false), ); -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { const user = interaction.options.getUser("user"); const container = new ContainerBuilder();

@@ -29,10 +35,13 @@ continue;

} for (const code of project.codes) { const codeText = code.expired ? `~~${code.code}~~` : `**${code.code}**`; - const addedString = code.addedAt ? `added ${tsRelative(code.addedAt)}` : undefined; - const lineParts = [`- ${codeText}`, addedString ? `(${addedString})` : undefined].filter( - Boolean, - ); + const addedString = code.addedAt + ? `added ${tsRelative(code.addedAt)}` + : undefined; + const lineParts = [ + `- ${codeText}`, + addedString ? `(${addedString})` : undefined, + ].filter(Boolean); containerTextArr.push(lineParts.join(" ")); }

@@ -52,7 +61,9 @@ container.addTextDisplayComponents(containerText, footerText);

await interaction.reply({ components: [container], flags: ["IsComponentsV2"], - ...(user ? { allowedMentions: { users: [user?.id] } } : { allowedMentions: { users: [] } }), + ...(user + ? { allowedMentions: { users: [user?.id] } } + : { allowedMentions: { users: [] } }), }); }
M src/interactions/commands/config.tssrc/interactions/commands/config.ts

@@ -48,7 +48,9 @@ { name: "remove", value: "remove" },

{ name: "list", value: "list" }, ), ) - .addRoleOption((opt) => opt.setName("role").setDescription("role to add/remove")), + .addRoleOption((opt) => + opt.setName("role").setDescription("role to add/remove"), + ), ) .addSubcommand((sub) => sub

@@ -74,19 +76,26 @@ .setDescription("value to set (for non-channel keys)")

.setRequired(false), ), ) - .addSubcommand((sub) => sub.setName("view").setDescription("view current server configuration")); + .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); + 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) { +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.",

@@ -144,7 +153,9 @@ const role = interaction.options.getRole("role") as Role | null;

const guildId = interaction.guild.id; const isDeveloper = interaction.user.id === cfg.developerId; - const hasPerms = await hasManagerPermissions(interaction.member as GuildMember); + const hasPerms = await hasManagerPermissions( + interaction.member as GuildMember, + ); if (!isDeveloper && !hasPerms) { await interaction.reply({

@@ -216,7 +227,10 @@ return;

} if (action === "remove") { - await db.delete(managerTable).where(eq(managerTable.roleId, role.id)).execute(); + await db + .delete(managerTable) + .where(eq(managerTable.roleId, role.id)) + .execute(); await interaction.reply({ content: `✅ Removed ${role} from manager roles.`,

@@ -256,7 +270,10 @@

await getGuild(guildId); if (isChannelKey(key)) { - const channel = interaction.options.getChannel("channel", false) as GuildChannel | null; + const channel = interaction.options.getChannel( + "channel", + false, + ) as GuildChannel | null; if (!channel) { await interaction.reply({
M src/interactions/commands/meow.tssrc/interactions/commands/meow.ts

@@ -1,8 +1,14 @@

import { text } from "@/utils/discord/components"; -import { type ChatInputCommandInteraction, type Client, SlashCommandBuilder } from "discord.js"; +import { + type ChatInputCommandInteraction, + type Client, + SlashCommandBuilder, +} from "discord.js"; import Uwuifier from "uwuifier"; -const commandData = new SlashCommandBuilder().setName("meow").setDescription("mrrp~"); +const commandData = new SlashCommandBuilder() + .setName("meow") + .setDescription("mrrp~"); const meowVariants = [ "meow",

@@ -87,7 +93,9 @@

// sometimes extend the meow with repeated letters const extendedMeow = Math.random() > 0.5 - ? meow.replace(/[aeiourw]/g, (m) => m.repeat(Math.floor(Math.random() * 4) + 1)) + ? meow.replace(/[aeiourw]/g, (m) => + m.repeat(Math.floor(Math.random() * 4) + 1), + ) : meow; words.push(`${prefix}${extendedMeow}${suffix}`);

@@ -95,17 +103,27 @@ }

return ( words.join(" ") + - (Math.random() > 0.4 ? ` ${emoticons[Math.floor(Math.random() * emoticons.length)]}` : "") + (Math.random() > 0.4 + ? ` ${emoticons[Math.floor(Math.random() * emoticons.length)]}` + : "") ); } -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { const uwuifier = new Uwuifier(); const meowText = generateMeow(); const uwuifiedMeowText = uwuifier.uwuifySentence(meowText); - const blacklistedWords = ["twerks", "sees bulge", "notices buldge", "starts twerking"]; + const blacklistedWords = [ + "twerks", + "sees bulge", + "notices buldge", + "starts twerking", + ]; const finalText = blacklistedWords.reduce((acc, word) => { return acc.replace(new RegExp(word, "gi"), "----");
M src/interactions/commands/pronouns.tssrc/interactions/commands/pronouns.ts

@@ -1,16 +1,23 @@

-import { type Client, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; +import { + type Client, + type ChatInputCommandInteraction, + SlashCommandBuilder, +} from "discord.js"; import { db, moderators } from "@/database"; import config from "@/config"; const pronounTypes = { subjective: "Subject (e.g., **They** are a moderator)", objective: "Object (e.g., Send **them** a message)", - "possessive-determiner": "Possessive Determiner (e.g., This is **their** post)", + "possessive-determiner": + "Possessive Determiner (e.g., This is **their** post)", "possessive-pronoun": "Possessive Pronoun (e.g., The post is **theirs**)", reflexive: "Reflexive (e.g., They assigned it to **themself**)", }; -const command = new SlashCommandBuilder().setName("woke").setDescription("Set your pronouns."); +const command = new SlashCommandBuilder() + .setName("woke") + .setDescription("Set your pronouns."); Object.entries(pronounTypes).forEach(([key, description]) => { command.addStringOption((option) =>

@@ -19,20 +26,35 @@ );

}); command.addUserOption((option) => - option.setName("user").setRequired(false).setDescription("The user to set pronouns for."), + option + .setName("user") + .setRequired(false) + .setDescription("The user to set pronouns for."), ); -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { const id = interaction.options.getUser("user")?.id || interaction.user.id; - if (interaction.options.getUser("user") && interaction.user.id !== config.developerId) { - await interaction.reply({ content: "You cannot use this command.", ephemeral: true }); + if ( + interaction.options.getUser("user") && + interaction.user.id !== config.developerId + ) { + await interaction.reply({ + content: "You cannot use this command.", + ephemeral: true, + }); return; } const pronounData = { subjective: interaction.options.getString("subjective", true), objective: interaction.options.getString("objective", true), - possessiveDeterminer: interaction.options.getString("possessive-determiner", true), + possessiveDeterminer: interaction.options.getString( + "possessive-determiner", + true, + ), possessive: interaction.options.getString("possessive-pronoun", true), reflexive: interaction.options.getString("reflexive", true), };

@@ -48,7 +70,10 @@ target: moderators.user_id,

set: pronounData, }); - await interaction.reply({ content: "Pronouns updated.", flags: ["Ephemeral"] }); + await interaction.reply({ + content: "Pronouns updated.", + flags: ["Ephemeral"], + }); } export default {
M src/interactions/commands/roblox/banHistory.tssrc/interactions/commands/roblox/banHistory.ts

@@ -18,7 +18,9 @@ import { hasManagerPermissions } from "@/utils/discord/permissions";

const commandData = new SlashCommandBuilder() .setName("ban-history") - .setDescription(`Get a user's ban history in the selected ${config.terminology}`) + .setDescription( + `Get a user's ban history in the selected ${config.terminology}`, + ) // .addSubcommand((subcommand) => // subcommand // .setName("discord")

@@ -50,7 +52,9 @@ .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") + .setDescription( + "The Roblox username to lookup - prefix with id: for IDs", + ) .setRequired(true), ) .addStringOption((option) =>

@@ -67,7 +71,10 @@ .setRequired(true),

), ); -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { const subcommand = interaction.options.getSubcommand(); const projectKey = interaction.options.getString( config.terminology,

@@ -77,7 +84,9 @@

const project = config.projects[projectKey]; const universeId = project.universe; - const isModerator = await hasManagerPermissions(interaction.member as GuildMember); + const isModerator = await hasManagerPermissions( + interaction.member as GuildMember, + ); await interaction.deferReply(); try {

@@ -89,7 +98,9 @@ const [linkedRes, linkedErr] = await roblox.getLinkedAccount(target.id);

if (linkedErr) { const message = linkedErr.error; if (message === "User not found") { - await interaction.editReply("No linked Roblox account found for that user."); + await interaction.editReply( + "No linked Roblox account found for that user.", + ); return; } await interaction.editReply(

@@ -122,14 +133,18 @@ await interaction.editReply(`Fetching user with ID ${id}...`);

const [user, userErr] = await roblox.getUser(id); if (userErr) { if (isApiErrorV2(userErr)) { - await interaction.editReply(`There was an error fetching the user: ${userErr.message}`); + await interaction.editReply( + `There was an error fetching the user: ${userErr.message}`, + ); } else { await interaction.editReply("There was an error fetching the user."); } return; } - await interaction.editReply(`Found user with username ${user.name}, fetching thumbnail...`); + await interaction.editReply( + `Found user with username ${user.name}, fetching thumbnail...`, + ); const [thumbnailRes] = await roblox.getThumbnail(id, { shape: "SQUARE" }); const thumbnailUri = thumbnailRes ? thumbnailRes.response?.imageUri : null;

@@ -141,7 +156,8 @@ "<:wawa:1158423817698430977>",

"<:max:1476776753845370991>", "<:snickerdoodle:1477173429575749716>", ]; - const message = randomMessages[Math.floor(Math.random() * randomMessages.length)]; + const message = + randomMessages[Math.floor(Math.random() * randomMessages.length)]; await interaction.editReply(message); const { mainContainer } = await constructUserContainer(user, {

@@ -162,14 +178,18 @@ components: [mainContainer, bansContainer],

}); } catch (err) { console.error(err); - await interaction.editReply("An unexpected error occurred while fetching the user."); + await interaction.editReply( + "An unexpected error occurred while fetching the user.", + ); } } async function buttonExecute(_client: Client, interaction: ButtonInteraction) { const [, action, userId] = interaction.customId.split(":"); - const isModerator = await hasManagerPermissions(interaction.member as GuildMember); + const isModerator = await hasManagerPermissions( + interaction.member as GuildMember, + ); const { container } = await constructBansSummaryContainer(userId, { showPrivate: isModerator, });
M src/interactions/commands/roblox/getUser.tssrc/interactions/commands/roblox/getUser.ts

@@ -31,14 +31,21 @@ .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") + .setDescription( + "The Roblox username to lookup - prefix with id: for IDs", + ) .setRequired(true), ), ); -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { const subcommand = interaction.options.getSubcommand(); - const isModerator = await hasManagerPermissions(interaction.member as GuildMember); + const isModerator = await hasManagerPermissions( + interaction.member as GuildMember, + ); await interaction.deferReply(); try {

@@ -50,7 +57,9 @@ const [linkedRes, linkedErr] = await roblox.getLinkedAccount(target.id);

if (linkedErr) { const message = linkedErr.error; if (message === "User not found") { - await interaction.editReply("No linked Roblox account found for that user."); + await interaction.editReply( + "No linked Roblox account found for that user.", + ); return; } await interaction.editReply(

@@ -82,7 +91,9 @@

await processRobloxUserInteraction(interaction, id, isModerator); } catch (err) { console.error(err); - await interaction.editReply("An unexpected error occurred while fetching the user."); + await interaction.editReply( + "An unexpected error occurred while fetching the user.", + ); } }
M src/interactions/commands/roblox/searchUsers.tssrc/interactions/commands/roblox/searchUsers.ts

@@ -11,10 +11,16 @@ const commandData = new SlashCommandBuilder()

.setName("search") .setDescription("Search for Roblox users") .addStringOption((option) => - option.setName("query").setDescription("The query to search for").setRequired(true), + option + .setName("query") + .setDescription("The query to search for") + .setRequired(true), ); -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { await interaction.deferReply(); const query = interaction.options.getString("query", true);

@@ -39,7 +45,9 @@ "*",

user.displayName, `[@${user.name}](https://www.roblox.com/users/${user.id}/profile)`, `(${user.id})`, - user.previousUsernames.length ? `(${user.previousUsernames.join(", ")})` : "", + user.previousUsernames.length + ? `(${user.previousUsernames.join(", ")})` + : "", ].join(" "), ) .join("\n");
M src/interactions/commands/stats.tssrc/interactions/commands/stats.ts

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

import { db, tickets } from "@/database"; -import { type ChatInputCommandInteraction, type Client, SlashCommandBuilder } from "discord.js"; +import { + type ChatInputCommandInteraction, + type Client, + SlashCommandBuilder, +} from "discord.js"; import { and, eq, gte, isNotNull, ne } from "drizzle-orm"; const commandData = new SlashCommandBuilder()

@@ -59,16 +63,22 @@ const mean = sum / durations.length;

const sorted = [...durations].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); - const median = sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; + const median = + sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; return { mean, median, count: durations.length }; } -async function execute(client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + client: Client, + interaction: ChatInputCommandInteraction, +) { await interaction.deferReply({}); const days = interaction.options.getInteger("days"); - const cutoffDate = days ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) : null; + const cutoffDate = days + ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) + : null; const closedTickets = await db .select()

@@ -79,7 +89,9 @@ isNotNull(tickets.closedAt),

isNotNull(tickets.closedBy), ne(tickets.closedBy, "reporter"), ne(tickets.closedBy, "system"), - interaction.guildId ? eq(tickets.guildId, interaction.guildId) : undefined, + interaction.guildId + ? eq(tickets.guildId, interaction.guildId) + : undefined, cutoffDate ? gte(tickets.closedAt, cutoffDate) : undefined, ), );

@@ -87,7 +99,9 @@

const timeFrameStr = days ? `(Last ${days} Days)` : "(All Time)"; if (closedTickets.length === 0) { - return interaction.editReply(`No closed tickets found for statistics ${timeFrameStr}.`); + return interaction.editReply( + `No closed tickets found for statistics ${timeFrameStr}.`, + ); } const sub = interaction.options.getSubcommand();

@@ -182,10 +196,12 @@ // biome-ignore lint/style/noNonNullAssertion: .

?.push(t.closedAt!.getTime() - t.createdAt.getTime()); } - const modStats = Array.from(modDurations.entries()).map(([modId, durations]) => { - const stats = getStats(durations); - return { modId, ...stats }; - }); + const modStats = Array.from(modDurations.entries()).map( + ([modId, durations]) => { + const stats = getStats(durations); + return { modId, ...stats }; + }, + ); const orderBy = interaction.options.getString("order_by") || "median";

@@ -214,9 +230,15 @@

const widths = { rank: Math.max(4, ...rowsData.map((r) => r.rank.length)), user: Math.max(8, ...rowsData.map((r) => r.username.length)), - count: Math.max(countTitle.length, ...rowsData.map((r) => r.count.length)), + count: Math.max( + countTitle.length, + ...rowsData.map((r) => r.count.length), + ), mean: Math.max(meanTitle.length, ...rowsData.map((r) => r.mean.length)), - median: Math.max(medianTitle.length, ...rowsData.map((r) => r.median.length)), + median: Math.max( + medianTitle.length, + ...rowsData.map((r) => r.median.length), + ), }; const header = [
M src/interactions/commands/ticket-manage.tssrc/interactions/commands/ticket-manage.ts

@@ -36,19 +36,27 @@ subcommand

.setName("prompt") .setDescription("Send the prompt to the designated channel") .addChannelOption((c) => - c.setName("channel").setDescription("The channel to send the prompt in").setRequired(true), + c + .setName("channel") + .setDescription("The channel to send the prompt in") + .setRequired(true), ), ) .addSubcommand((subcommand) => subcommand .setName("message") - .setDescription("Edit the saved ticket message (shown when a ticket is created)"), + .setDescription( + "Edit the saved ticket message (shown when a ticket is created)", + ), ); const MODAL_ID = makeComponentId(commandData, "message"); const INPUT_ID = "message_content"; -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +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.",

@@ -57,7 +65,9 @@ });

return; } - const hasPerms = await hasManagerPermissions(interaction.member as GuildMember); + const hasPerms = await hasManagerPermissions( + interaction.member as GuildMember, + ); if (!hasPerms) { await interaction.reply({ content: "❌ You don't have permission to use this command.",

@@ -73,7 +83,9 @@ const targetChannel = interaction.options.getChannel("channel", true);

const textInput = new LabelBuilder() .setLabel("Message Content") - .setDescription("This message will be followed by a button to create a new ticket.") + .setDescription( + "This message will be followed by a button to create a new ticket.", + ) .setTextInputComponent( new TextInputBuilder() .setCustomId(INPUT_ID)

@@ -91,11 +103,15 @@ return;

} if (sub === "message") { - const { data: currentMessage } = await getTicketMessage(interaction.guild.id); + const { data: currentMessage } = await getTicketMessage( + interaction.guild.id, + ); const textInput = new LabelBuilder() .setLabel("Message Content") - .setDescription("This message will be shown to users when they create a ticket.") + .setDescription( + "This message will be shown to users when they create a ticket.", + ) .setTextInputComponent( new TextInputBuilder() .setCustomId(INPUT_ID)

@@ -114,7 +130,10 @@ return;

} } -async function modalExecute(_client: Client, interaction: ModalSubmitInteraction) { +async function modalExecute( + _client: Client, + interaction: ModalSubmitInteraction, +) { await interaction.deferReply({ flags: ["Ephemeral"] }); if (!interaction.channelId || !interaction.guild || !interaction.guildId) { await interaction.editReply({
M src/interactions/commands/tickets/_shared.tssrc/interactions/commands/tickets/_shared.ts

@@ -32,7 +32,9 @@ } as const;

// -- modals --------------------------------------------------------------------------------------- export function getTicketReasonModal(staff = false) { - const modal = new ModalBuilder().setCustomId(MODAL_IDS.reason).setTitle("Close Ticket"); + const modal = new ModalBuilder() + .setCustomId(MODAL_IDS.reason) + .setTitle("Close Ticket"); const pubReason = new LabelBuilder() .setLabel("Public Reason")

@@ -52,7 +54,9 @@ );

const privateReason = new LabelBuilder() .setLabel("Private Reason") - .setDescription("Provide a reason for closing the ticket - this will only be visible to staff") + .setDescription( + "Provide a reason for closing the ticket - this will only be visible to staff", + ) .setTextInputComponent( new TextInputBuilder() .setCustomId(INPUT_IDS.PRIVATE_REASON)

@@ -65,7 +69,9 @@

const deleteGroup = new LabelBuilder() .setLabel("Immediately delete channel") .setCheckboxComponent( - new CheckboxBuilder().setCustomId("ticket:delete_checkbox").setDefault(false), + new CheckboxBuilder() + .setCustomId("ticket:delete_checkbox") + .setDefault(false), ); modal.addLabelComponents(pubReason);

@@ -116,8 +122,12 @@ );

} const wasBehalfTicket = ticket.openedBy !== ticket.authorId; - const reasonText = ticket.topic ? `for reason: ${ticket.topic}` : "without a provided reason"; - const behalfText = wasBehalfTicket ? `**Opened By**: <@${ticket.openedBy}> ${reasonText}` : null; + const reasonText = ticket.topic + ? `for reason: ${ticket.topic}` + : "without a provided reason"; + const behalfText = wasBehalfTicket + ? `**Opened By**: <@${ticket.openedBy}> ${reasonText}` + : null; const container = new ContainerBuilder() .addTextDisplayComponents(

@@ -140,7 +150,9 @@ .filter(Boolean)

.join("\n"), ), ) - .addSeparatorComponents(new SeparatorBuilder().setDivider(false).setSpacing(2)) + .addSeparatorComponents( + new SeparatorBuilder().setDivider(false).setSpacing(2), + ) .addTextDisplayComponents( text( [

@@ -175,7 +187,12 @@ )

.join(", "); return { - container: constructTicketContainer(ticket, attachmentsString, context, isPublic), + container: constructTicketContainer( + ticket, + attachmentsString, + context, + isPublic, + ), transcriptText, attachments, files: [file],
M src/interactions/commands/tickets/actions.tssrc/interactions/commands/tickets/actions.ts

@@ -1,5 +1,10 @@

import { type Guild, type Ticket, db, tickets } from "@/database"; -import { getGuild, getManagerRoleIds, getTicket, getTicketById } from "@/database/queries"; +import { + getGuild, + getManagerRoleIds, + getTicket, + getTicketById, +} from "@/database/queries"; import { text } from "@/utils/discord/components"; import { hasManagerPermissions } from "@/utils/discord/permissions"; import { loggers } from "@/utils/logging";

@@ -46,10 +51,13 @@ if (

interaction instanceof ChatInputCommandInteraction && interaction.options.getSubcommand() === "create-for" ) { - const isModerator = await hasManagerPermissions(interaction.member as GuildMember); + const isModerator = await hasManagerPermissions( + interaction.member as GuildMember, + ); if (!isModerator) { await interaction.reply({ - content: "❌ You don't have permission to create tickets for other users.", + content: + "❌ You don't have permission to create tickets for other users.", }); }

@@ -100,7 +108,10 @@ ],

}, { id: client.user.id, - allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels], + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.ManageChannels, + ], }, ...managerRoleIds.map((roleId) => ({ id: roleId,

@@ -134,7 +145,10 @@ targetUser ? targetUser.id : interaction.user.id,

); const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder().setCustomId("ticket:claim").setLabel("Claim").setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("ticket:claim") + .setLabel("Claim") + .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("ticket:close") .setLabel("Close")

@@ -175,7 +189,8 @@ });

const canDm = await canDmUser(targetUser ?? interaction.user); if (!canDm) { - const formatCommand = (name: string) => `</ticket ${name}:1477532047785332906>`; + const formatCommand = (name: string) => + `</ticket ${name}:1477532047785332906>`; const view = formatCommand("view"); const list = formatCommand("list");

@@ -211,11 +226,15 @@ ticket: Ticket,

) { const targetUser = interaction.options.getUser("user", true); - const isStaff = await hasManagerPermissions(interaction.member as GuildMember); + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); const isAuthor = interaction.user.id === ticket.authorId; if (!isStaff && !isAuthor) { - await interaction.editReply("❌ You don't have permission to add users to this ticket."); + await interaction.editReply( + "❌ You don't have permission to add users to this ticket.", + ); return; }

@@ -229,8 +248,13 @@ SendMessages: true,

AttachFiles: true, }); - const updatedUsers = [...new Set([...(ticket.addedUsers || []), targetUser.id])]; - await db.update(tickets).set({ addedUsers: updatedUsers }).where(eq(tickets.id, ticket.id)); + const updatedUsers = [ + ...new Set([...(ticket.addedUsers || []), targetUser.id]), + ]; + await db + .update(tickets) + .set({ addedUsers: updatedUsers }) + .where(eq(tickets.id, ticket.id)); await channel.send({ content: `✅ <@${targetUser.id}> has been added to the ticket by <@${interaction.user.id}>.`,

@@ -238,8 +262,13 @@ });

await interaction.editReply("✅ User added successfully."); } catch (error) { - logger.error(error, `Failed to add user ${targetUser.id} to ticket ${ticket.id}`); - await interaction.editReply("❌ Failed to add the user to the channel permissions."); + logger.error( + error, + `Failed to add user ${targetUser.id} to ticket ${ticket.id}`, + ); + await interaction.editReply( + "❌ Failed to add the user to the channel permissions.", + ); } }

@@ -266,7 +295,9 @@ await interaction.editReply("❌ This ticket is already closed.");

return; } const closedByReporter = interaction.user.id === _ticket.authorId; - const isManager = await hasManagerPermissions(interaction.member as GuildMember); + const isManager = await hasManagerPermissions( + interaction.member as GuildMember, + ); const channel = interaction.channel; if (!channel || !("permissionOverwrites" in channel)) return;

@@ -284,7 +315,10 @@ closedBy: closedByReporter ? "reporter" : interaction.user.id,

}) .where(eq(tickets.id, _ticket.id)); - const { data: ticket, exists } = await getTicket(interaction.guildId, interaction.channelId); + const { data: ticket, exists } = await getTicket( + interaction.guildId, + interaction.channelId, + ); if (!exists || !ticket) { await interaction.editReply({

@@ -294,7 +328,9 @@ return;

} try { - const member = await interaction.guild?.members.fetch(ticket.authorId).catch(() => null); + const member = await interaction.guild?.members + .fetch(ticket.authorId) + .catch(() => null); const isAdmin = member?.permissions.has(PermissionFlagsBits.Administrator); const isOwner = interaction.guild?.ownerId === ticket.authorId;

@@ -348,7 +384,10 @@ try {

await fn(); return; } catch (error) { - logger.error(error, `Attempt ${attempt}/${maxRetries} failed for ${label}`); + logger.error( + error, + `Attempt ${attempt}/${maxRetries} failed for ${label}`, + ); if (attempt === maxRetries) { throw error; }

@@ -367,7 +406,10 @@ users: [],

}, }); } catch (error) { - if (error instanceof DiscordAPIError && [50007, 50278].includes(error.code as number)) { + if ( + error instanceof DiscordAPIError && + [50007, 50278].includes(error.code as number) + ) { channel.send(`Couldn't DM the reporter, they may have DMs disabled.`); } else { channel.send("Couldn't DM the reporter.");

@@ -387,7 +429,8 @@ `No transcript channel configured for guild ${interaction.guildId}, skipping transcript post`,

); return; } - const transcriptChannel = await client.channels.fetch(transcriptChannelId); + const transcriptChannel = + await client.channels.fetch(transcriptChannelId); if (!transcriptChannel || !("send" in transcriptChannel)) { logger.warn( `Transcript channel with ID ${transcriptChannelId} not found or is not a text channel`,

@@ -408,13 +451,17 @@ error,

`Could not send ticket transcript to transcript channel ${transcriptChannelId} for ticket ${ticket.id}`, ); await interaction.followUp({ - content: "⚠️ Ticket closed but failed to send transcript to transcript channel.", + content: + "⚠️ Ticket closed but failed to send transcript to transcript channel.", }); } }; try { - await retry(messageAuthor, `DM user ${ticket.authorId} for ticket ${ticket.id}`); + await retry( + messageAuthor, + `DM user ${ticket.authorId} for ticket ${ticket.id}`, + ); await retry( sendToStaff, `send transcript to channel ${transcriptChannelId} for ticket ${ticket.id}`,

@@ -437,7 +484,10 @@ name: `closed-${channelName}`,

}); } -export async function handleThank(_client: Client, interaction: ButtonInteraction) { +export async function handleThank( + _client: Client, + interaction: ButtonInteraction, +) { await interaction.deferReply({ flags: ["Ephemeral"] }); const id = Number(interaction.customId.split(":")[2]);

@@ -452,7 +502,9 @@ );

} if (ticket.thankedAt) { - return interaction.editReply("⚠️ You have already thanked the moderator for this ticket."); + return interaction.editReply( + "⚠️ You have already thanked the moderator for this ticket.", + ); } const { data: guild, exists: guildExists } = await getGuild(ticket.guildId);

@@ -479,12 +531,20 @@ `The reporter says thank you for handling ticket #${ticket.ticketNumber}! 💖`,

); const thankedAt = new Date(); - await db.update(tickets).set({ thankedAt }).where(eq(tickets.id, ticket.id)); + await db + .update(tickets) + .set({ thankedAt }) + .where(eq(tickets.id, ticket.id)); return interaction.editReply("A thank you message sent to the moderators!"); } catch (error) { - logger.error(error, `Could not send thank you message for ticket ${ticket.id}`); - return interaction.editReply("⚠️ Failed to send thank you message to the moderator."); + logger.error( + error, + `Could not send thank you message for ticket ${ticket.id}`, + ); + return interaction.editReply( + "⚠️ Failed to send thank you message to the moderator.", + ); } }

@@ -528,7 +588,10 @@ logger.error(error, `could not restore permissions for ${ticket.authorId}`);

} const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder().setCustomId("ticket:claim").setLabel("Claim").setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("ticket:claim") + .setLabel("Claim") + .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("ticket:close") .setLabel("Close")

@@ -549,7 +612,9 @@ _client: Client,

interaction: ButtonInteraction | ChatInputCommandInteraction, ticket: Ticket, ) { - const isStaff = await hasManagerPermissions(interaction.member as GuildMember); + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); if (!isStaff) { await interaction.editReply( "❌ You don't have permission to delete this ticket, you can only close it.",

@@ -557,9 +622,14 @@ );

return; } - await db.update(tickets).set({ status: "archived" }).where(eq(tickets.id, ticket.id)); + await db + .update(tickets) + .set({ status: "archived" }) + .where(eq(tickets.id, ticket.id)); - await interaction.editReply("✅ Ticket deleted. Channel will be deleted shortly."); + await interaction.editReply( + "✅ Ticket deleted. Channel will be deleted shortly.", + ); const channel = interaction.channel; if (channel && "delete" in channel) {

@@ -569,7 +639,9 @@ }

logger.error( `Channel with ID ${interaction.channelId} not found or cannot be deleted for ticket ${ticket.id}`, ); - interaction.editReply("Ticket was marked as deleted but couldn't delete the channel."); + interaction.editReply( + "Ticket was marked as deleted but couldn't delete the channel.", + ); } export async function handleTranscript(

@@ -577,7 +649,9 @@ _client: Client,

interaction: ButtonInteraction | ChatInputCommandInteraction, ticket: Ticket, ) { - const isModerator = await hasManagerPermissions(interaction.member as GuildMember); + const isModerator = await hasManagerPermissions( + interaction.member as GuildMember, + ); const isReporter = interaction.user.id === ticket.authorId; if (!isModerator && !isReporter) { await interaction.editReply(

@@ -606,7 +680,9 @@ _client: Client,

interaction: ButtonInteraction | ChatInputCommandInteraction, ticket: Ticket, ) { - const isStaff = await hasManagerPermissions(interaction.member as GuildMember); + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); if (!isStaff) { await interaction.editReply({ content: "Only staff can claim tickets.",

@@ -641,7 +717,10 @@ .setStyle(ButtonStyle.Danger)

.setEmoji("🔒"), ); - if (interaction.channel?.isSendable() && interaction.channel.type === ChannelType.GuildText) { + if ( + interaction.channel?.isSendable() && + interaction.channel.type === ChannelType.GuildText + ) { const moderator = getModerator(interaction.user.id); const pronoun = capitalise(moderator.subjective);

@@ -649,7 +728,9 @@ await interaction.channel.send({

content: `This ticket has been claimed by <@${interaction.user.id}>! ${pronoun} will be with you shortly.`, components: [controls], }); - interaction.channel.setName(`claimed-${ticket.ticketNumber}`).catch(() => {}); + interaction.channel + .setName(`claimed-${ticket.ticketNumber}`) + .catch(() => {}); } await interaction.editReply("✅ Ticket claimed.");

@@ -660,7 +741,9 @@ _client: Client,

interaction: ButtonInteraction | ChatInputCommandInteraction, ticket: Ticket, ) { - const isStaff = await hasManagerPermissions(interaction.member as GuildMember); + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); if (!isStaff) { await interaction.editReply({ content: "❌ Only staff can unclaim tickets.",

@@ -684,7 +767,10 @@ })

.where(eq(tickets.id, ticket.id)); const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder().setCustomId("ticket:claim").setLabel("Claim").setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("ticket:claim") + .setLabel("Claim") + .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("ticket:close") .setLabel("Close")

@@ -694,7 +780,8 @@ );

if (interaction.channel?.isSendable()) { await interaction.channel.send({ - content: "This ticket is no longer claimed, someone else will be with you shortly.", + content: + "This ticket is no longer claimed, someone else will be with you shortly.", components: [controls], }); }
M src/interactions/commands/tickets/gdpr.tssrc/interactions/commands/tickets/gdpr.ts

@@ -24,13 +24,17 @@ async function buildListContainerForUser(userId: string, page = 1) {

const ticketsRes = await db .select() .from(tickets) - .where(or(eq(tickets.authorId, userId), like(tickets.addedUsers, `%${userId}%`))); + .where( + or(eq(tickets.authorId, userId), like(tickets.addedUsers, `%${userId}%`)), + ); if (!ticketsRes || ticketsRes.length === 0) { const empty = new ContainerBuilder() .setAccentColor(0xff5a00) .addTextDisplayComponents( - new TextDisplayBuilder().setContent("## Your Tickets\n\nYou have no tickets."), + new TextDisplayBuilder().setContent( + "## Your Tickets\n\nYou have no tickets.", + ), ); return { container: empty, totalPages: 0, page: 1, total: 0 }; }

@@ -77,7 +81,9 @@ .setAccentColor(0xff5a00)

.addTextDisplayComponents( new TextDisplayBuilder().setContent(`${header}${ticketLines.join("\n")}`), ) - .addSeparatorComponents(new SeparatorBuilder().setDivider(false).setSpacing(1)); + .addSeparatorComponents( + new SeparatorBuilder().setDivider(false).setSpacing(1), + ); const controls = new ActionRowBuilder<ButtonBuilder>().addComponents( new ButtonBuilder()

@@ -104,7 +110,10 @@

return { container, totalPages, page: currentPage, total }; } -export async function handleListPagination(_client: Client, interaction: ButtonInteraction) { +export async function handleListPagination( + _client: Client, + interaction: ButtonInteraction, +) { const parts = interaction.customId.split(":"); const pagePart = parts[3]; const page = pagePart ? Number.parseInt(pagePart, 10) : 1;

@@ -119,7 +128,10 @@ } catch {}

return; } - const { container } = await buildListContainerForUser(interaction.user.id, page); + const { container } = await buildListContainerForUser( + interaction.user.id, + page, + ); try { await interaction.update({ components: [container] });

@@ -134,7 +146,10 @@ logger.error(err, "failed to update ticket list pagination");

} } -export async function handleList(_client: Client, interaction: ChatInputCommandInteraction) { +export async function handleList( + _client: Client, + interaction: ChatInputCommandInteraction, +) { if (!interaction.guild) { await interaction.reply({ content: "❌ This command can only be used in a server.",

@@ -151,7 +166,10 @@ flags: ["IsComponentsV2", "Ephemeral"],

}); } -export async function handleViewTicket(_client: Client, interaction: ChatInputCommandInteraction) { +export async function handleViewTicket( + _client: Client, + interaction: ChatInputCommandInteraction, +) { await interaction.deferReply(); if (!interaction.guild) { await interaction.editReply({

@@ -162,7 +180,10 @@ }

const ticketId = interaction.options.getInteger("ticket_id", true); - const { data: ticket, exists } = await getTicketByNumericId(interaction.guild.id, ticketId); + const { data: ticket, exists } = await getTicketByNumericId( + interaction.guild.id, + ticketId, + ); if (!exists || !ticket) { await interaction.editReply({ content: `❌ Ticket with ID ${ticketId} not found.`,

@@ -170,9 +191,12 @@ });

return; } - const isStaff = await hasManagerPermissions(interaction.member as GuildMember); + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); const isAuthor = - ticket.authorId === interaction.user.id || ticket.addedUsers?.includes(interaction.user.id); + ticket.authorId === interaction.user.id || + ticket.addedUsers?.includes(interaction.user.id); if (!isStaff && !isAuthor) { await interaction.editReply({

@@ -181,7 +205,11 @@ });

return; } - const { container, files } = await getTicketContainer(ticket, "view", !isStaff); + const { container, files } = await getTicketContainer( + ticket, + "view", + !isStaff, + ); await interaction.editReply({ components: [container],

@@ -190,7 +218,10 @@ flags: ["IsComponentsV2"],

}); } -export async function handleExport(_client: Client, interaction: ChatInputCommandInteraction) { +export async function handleExport( + _client: Client, + interaction: ChatInputCommandInteraction, +) { const _tickets = await db .select() .from(tickets)
M src/interactions/commands/tickets/index.tssrc/interactions/commands/tickets/index.ts

@@ -2,7 +2,7 @@ import {

type ButtonInteraction, type ChatInputCommandInteraction, type Client, - GuildMember, + type GuildMember, type ModalSubmitInteraction, SlashCommandBuilder, SlashCommandSubcommandBuilder,

@@ -24,7 +24,12 @@ handleThank,

handleTranscript, handleUnclaim, } from "./actions"; -import { handleExport, handleList, handleListPagination, handleViewTicket } from "./gdpr"; +import { + handleExport, + handleList, + handleListPagination, + handleViewTicket, +} from "./gdpr"; import { handleReasonModal } from "./modals"; import { hasManagerPermissions } from "@/utils/discord/permissions";

@@ -33,7 +38,9 @@ .setName("ticket")

.setDescription("Manage and create new tickets") // create .addSubcommand( - new SlashCommandSubcommandBuilder().setName("create").setDescription("Create a new ticket"), + new SlashCommandSubcommandBuilder() + .setName("create") + .setDescription("Create a new ticket"), ) // create-for .addSubcommand(

@@ -59,7 +66,10 @@ new SlashCommandSubcommandBuilder()

.setName("add") .setDescription("Add another user to the ticket") .addUserOption((option) => - option.setName("user").setDescription("The user to add").setRequired(true), + option + .setName("user") + .setDescription("The user to add") + .setRequired(true), ), ) // claim

@@ -88,7 +98,9 @@ )

.addStringOption((option) => option .setName("private_reason") - .setDescription("Private reason for closing the ticket, shown only to staff") + .setDescription( + "Private reason for closing the ticket, shown only to staff", + ) .setRequired(false), ), )

@@ -108,7 +120,9 @@ // transcript

.addSubcommand( new SlashCommandSubcommandBuilder() .setName("transcript") - .setDescription("Get the transcript of the ticket in the current channel"), + .setDescription( + "Get the transcript of the ticket in the current channel", + ), ) // list .addSubcommand(

@@ -132,10 +146,15 @@ // export

.addSubcommand( new SlashCommandSubcommandBuilder() .setName("export") - .setDescription("Export all of your ticket data in a machine readable format"), + .setDescription( + "Export all of your ticket data in a machine readable format", + ), ); -async function execute(client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + client: Client, + interaction: ChatInputCommandInteraction, +) { const action = interaction.options.getSubcommand(); const actions = {

@@ -159,7 +178,10 @@ return;

} const { data: guild } = await getGuild(interaction.guildId); - const { data: ticket } = await getTicket(interaction.guildId, interaction.channelId); + const { data: ticket } = await getTicket( + interaction.guildId, + interaction.channelId, + ); if (!ticket) { await interaction.reply({

@@ -183,7 +205,8 @@ }

} const publicReason = interaction.options.getString("reason") ?? undefined; - const privateReason = interaction.options.getString("private_reason") ?? undefined; + const privateReason = + interaction.options.getString("private_reason") ?? undefined; closeLock.set(ticket.id, { expiresAt: Date.now() + thirtySeconds,

@@ -212,17 +235,21 @@ });

return; } - const isStaff = await hasManagerPermissions(interaction.member as GuildMember); + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); return await interaction.showModal(getTicketReasonModal(isStaff)); } await interaction.deferReply({ flags: ["Ephemeral"] }); if (action === "reopen") await handleReopen(client, interaction, ticket); else if (action === "delete") await handleDelete(client, interaction, ticket); - else if (action === "transcript") await handleTranscript(client, interaction, ticket); + else if (action === "transcript") + await handleTranscript(client, interaction, ticket); else if (action === "add") await handleAddUser(client, interaction, ticket); else if (action === "claim") await handleClaim(client, interaction, ticket); - else if (action === "unclaim") await handleUnclaim(client, interaction, ticket); + else if (action === "unclaim") + await handleUnclaim(client, interaction, ticket); else await interaction.editReply("❌ Invalid or unhandled action."); }

@@ -250,7 +277,10 @@ await handleCreate(client, interaction);

return; } - const { data: ticket } = await getTicket(interaction.guildId, interaction.channelId); + const { data: ticket } = await getTicket( + interaction.guildId, + interaction.channelId, + ); if (!ticket) { await interaction.reply({

@@ -260,20 +290,27 @@ return;

} if (action === "close") { - const isStaff = await hasManagerPermissions(interaction.member as GuildMember); + const isStaff = await hasManagerPermissions( + interaction.member as GuildMember, + ); return await interaction.showModal(getTicketReasonModal(isStaff)); } await interaction.deferReply({ flags: ["Ephemeral"] }); if (action === "reopen") await handleReopen(client, interaction, ticket); else if (action === "delete") await handleDelete(client, interaction, ticket); - else if (action === "transcript") await handleTranscript(client, interaction, ticket); + else if (action === "transcript") + await handleTranscript(client, interaction, ticket); else if (action === "claim") await handleClaim(client, interaction, ticket); - else if (action === "unclaim") await handleUnclaim(client, interaction, ticket); + else if (action === "unclaim") + await handleUnclaim(client, interaction, ticket); else await interaction.editReply("❌ Invalid or unhandled action."); } -async function modalExecute(client: Client, interaction: ModalSubmitInteraction) { +async function modalExecute( + client: Client, + interaction: ModalSubmitInteraction, +) { const components = parseComponentId(interaction.customId); const action = components.parts[0];
M src/interactions/commands/tickets/modals.tssrc/interactions/commands/tickets/modals.ts

@@ -6,7 +6,10 @@ import { handleClose } from "./actions";

const logger = loggers.interactions.child({ name: "ticket/modals" }); -export async function handleReasonModal(_client: Client, interaction: ModalSubmitInteraction) { +export async function handleReasonModal( + _client: Client, + interaction: ModalSubmitInteraction, +) { await interaction.deferReply({ flags: ["Ephemeral"] }); if (!interaction.channelId || !interaction.guildId) {

@@ -19,12 +22,19 @@

const publicReason = interaction.fields.getTextInputValue(INPUT_IDS.REASON); let privateReason = ""; try { - privateReason = interaction.fields.getTextInputValue(INPUT_IDS.PRIVATE_REASON); + privateReason = interaction.fields.getTextInputValue( + INPUT_IDS.PRIVATE_REASON, + ); } catch {} - const deleteChannel = interaction.fields.getCheckbox("ticket:delete_checkbox"); + const deleteChannel = interaction.fields.getCheckbox( + "ticket:delete_checkbox", + ); - const { data: ticket, exists } = await getTicket(interaction.guildId, interaction.channelId); + const { data: ticket, exists } = await getTicket( + interaction.guildId, + interaction.channelId, + ); if (!exists || !ticket) { await interaction.editReply({ content: "❌ This channel is not a valid ticket.",
M src/interactions/commands/uwuify.tssrc/interactions/commands/uwuify.ts

@@ -1,20 +1,35 @@

-import { type ChatInputCommandInteraction, type Client, SlashCommandBuilder } from "discord.js"; +import { + type ChatInputCommandInteraction, + type Client, + SlashCommandBuilder, +} from "discord.js"; import Uwuifier from "uwuifier"; const commandData = new SlashCommandBuilder() .setName("uwuify") .setDescription("ok") .addStringOption((option) => - option.setName("text").setDescription("The text to uwuify").setRequired(true), + option + .setName("text") + .setDescription("The text to uwuify") + .setRequired(true), ); -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { const uwuifier = new Uwuifier(); const text = interaction.options.getString("text", true); const uwuifiedText = uwuifier.uwuifySentence(text); - const blacklistedWords = ["twerks", "sees bulge", "notices buldge", "starts twerking"]; + const blacklistedWords = [ + "twerks", + "sees bulge", + "notices buldge", + "starts twerking", + ]; const finalText = blacklistedWords.reduce((acc, word) => { return acc.replace(new RegExp(word, "gi"), "----");
M src/interactions/commands/uwuifyProxy.tssrc/interactions/commands/uwuifyProxy.ts

@@ -1,12 +1,19 @@

import { db, userPreferences } from "@/database"; -import { type ChatInputCommandInteraction, type Client, SlashCommandBuilder } from "discord.js"; +import { + type ChatInputCommandInteraction, + type Client, + SlashCommandBuilder, +} from "discord.js"; import { eq } from "drizzle-orm"; const commandData = new SlashCommandBuilder() .setName("uwuify-toggle") .setDescription("toggle automatic uwuification of your messages"); -async function execute(_client: Client, interaction: ChatInputCommandInteraction) { +async function execute( + _client: Client, + interaction: ChatInputCommandInteraction, +) { const userId = interaction.user.id; const existing = db
M src/interactions/menus/getUser.tssrc/interactions/menus/getUser.ts

@@ -14,8 +14,13 @@ const commandData = new ContextMenuCommandBuilder()

.setName("Get Roblox Account") .setType(ApplicationCommandType.User); -async function execute(_client: Client, interaction: MessageContextMenuCommandInteraction) { - const isModerator = await hasManagerPermissions(interaction.member as GuildMember); +async function execute( + _client: Client, + interaction: MessageContextMenuCommandInteraction, +) { + const isModerator = await hasManagerPermissions( + interaction.member as GuildMember, + ); await interaction.deferReply(); const target = interaction.targetId;

@@ -24,7 +29,9 @@

if (linkedErr) { const message = linkedErr.error; if (message === "User not found") { - await interaction.editReply("No linked Roblox account found for that user."); + await interaction.editReply( + "No linked Roblox account found for that user.", + ); return; } await interaction.editReply(
M src/interactions/menus/highlight.tssrc/interactions/menus/highlight.ts

@@ -42,7 +42,8 @@ }

try { const url = new URL(t); - if (!domains.some((d) => url.hostname.toLowerCase().includes(d))) continue; + if (!domains.some((d) => url.hostname.toLowerCase().includes(d))) + continue; const normalized = url.href.replace(/\/$/, ""); found.add(normalized);

@@ -53,10 +54,16 @@ return Array.from(found);

} function isVideoAttachment(attachment: Attachment): boolean { - return typeof attachment.contentType === "string" && attachment.contentType.startsWith("video/"); + return ( + typeof attachment.contentType === "string" && + attachment.contentType.startsWith("video/") + ); } -async function execute(client: Client, interaction: MessageContextMenuCommandInteraction) { +async function execute( + client: Client, + interaction: MessageContextMenuCommandInteraction, +) { if (!interaction.guild) { await interaction.reply({ content: "❌ This command can only be used in a server.",

@@ -76,7 +83,8 @@ }

if (!guildConfig.highlights_channel_id) { await interaction.editReply({ - content: "❌ No highlights channel configured. Use `/config highlight-channel`.", + content: + "❌ No highlights channel configured. Use `/config highlight-channel`.", }); return; }

@@ -94,7 +102,8 @@ 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.", + content: + "❌ No video attachments or supported links found in this message.", }); return; }

@@ -124,7 +133,9 @@ const jumpUrl = targetMessage.url;

if (videoLinks.length === 0) videoLinks.push(...files); - const videoLinksText = videoLinks.map((link) => `• [Video Link](${link})`).join(" "); + const videoLinksText = videoLinks + .map((link) => `• [Video Link](${link})`) + .join(" "); const description = `:star: New highlight from ${author}!\n-# [Jump to message](${jumpUrl}) ${videoLinksText}`; try {
M src/roblox/client.tssrc/roblox/client.ts

@@ -109,7 +109,9 @@

const body = await res.json(); if (!res.ok || isApiError(body)) { - const err: ApiError = isApiError(body) ? body : errorFromStatus(res.status, res.statusText); + const err: ApiError = isApiError(body) + ? body + : errorFromStatus(res.status, res.statusText); return [null, err]; }

@@ -130,7 +132,9 @@ let pages = 0;

const MAX_PAGES = 50; while (pages < MAX_PAGES) { - const filter = encodeURIComponent(`place == '' && user == 'users/${userId}'`); + const filter = encodeURIComponent( + `place == '' && user == 'users/${userId}'`, + ); const path = `/user/cloud/v2/universes/${universeId}/user-restrictions:listLogs?filter=${filter}&pageToken=${pageToken}`; const [data, err] = await this.request<UserRestrictionLogsResponse>(path);

@@ -155,7 +159,9 @@

return [data, null]; } - async getUsersByUsernames(usernames: string[]): Promise<Result<GetUsersByUsernamesResponse>> { + async getUsersByUsernames( + usernames: string[], + ): Promise<Result<GetUsersByUsernamesResponse>> { const payload: GetUsersByUsernamesPayload = { usernames, excludeBannedUsers: false,

@@ -206,11 +212,14 @@ return [data, null];

} async getLinkedAccount(discordId: string): Promise<BloxlinkResult> { - const response = await fetch(`https://api.blox.link/v4/public/discord-to-roblox/${discordId}`, { - headers: { - Authorization: config.roblox.bloxlinkToken, + const response = await fetch( + `https://api.blox.link/v4/public/discord-to-roblox/${discordId}`, + { + headers: { + Authorization: config.roblox.bloxlinkToken, + }, }, - }); + ); const data = await response.json();

@@ -220,4 +229,7 @@ return [data, null];

} } -export const roblox = new RobloxClient(config.roblox.cookie, config.roblox.apiKey); +export const roblox = new RobloxClient( + config.roblox.cookie, + config.roblox.apiKey, +);
M src/roblox/profile.tssrc/roblox/profile.ts

@@ -1,6 +1,10 @@

import config, { type Project } from "@/config"; import { roblox } from "@/roblox/client"; -import type { GetUserResponse, RobloxUserId, UserRestrictionLog } from "@/roblox/types"; +import type { + GetUserResponse, + RobloxUserId, + UserRestrictionLog, +} from "@/roblox/types"; import { ActionRowBuilder, ButtonBuilder,

@@ -97,7 +101,9 @@ // ---- moderators -------------------------------------------------------------

const ModeratorCache = new Map<RobloxUserId, string>(); export async function moderatorMention( - moderator: { robloxUser: `users/${RobloxUserId}` } | { gameServerScript: unknown }, + moderator: + | { robloxUser: `users/${RobloxUserId}` } + | { gameServerScript: unknown }, ) { if ("gameServerScript" in moderator) { return "Banned in-game";

@@ -140,7 +146,8 @@ const now = Date.now();

return logs.map((log) => { const startMs = new Date(log.startTime).getTime(); const durationMs = durationToMs(log.duration ?? undefined); - const endMs = durationMs == null ? Number.POSITIVE_INFINITY : startMs + durationMs; + const endMs = + durationMs == null ? Number.POSITIVE_INFINITY : startMs + durationMs; const nowActive = now < endMs; return { ...(log as UserRestrictionLog),

@@ -148,7 +155,9 @@ startMs,

endMs, nowActive, ...(log.universeId ? { universeId: log.universeId } : {}), - project: Object.values(config.projects).find((p) => p.universe === log.universeId), + project: Object.values(config.projects).find( + (p) => p.universe === log.universeId, + ), }; }); }

@@ -178,26 +187,37 @@ startDate: new Date(log.createTime),

}); const endString = - endAbsolute === "permanent" ? undefined : `> **Ends:** ${endAbsolute} (${endRelative})`; + endAbsolute === "permanent" + ? undefined + : `> **Ends:** ${endAbsolute} (${endRelative})`; const lines = [ includeProjectLabel ? `**${includeProjectLabel}**` : undefined, `> Banned **${bannedFor}**, ${startRel}, by **${moderator}**`, `> **Reason:** ${reason}`, - showPrivate ? `> **Private Reason:** ${log.privateReason || "no private reason"}` : undefined, + showPrivate + ? `> **Private Reason:** ${log.privateReason || "no private reason"}` + : undefined, endString, ].filter(Boolean); return lines.join("\n"); } -function buildLogsContainer(headerTitle: string, lines: string[], shown: number, total: number) { +function buildLogsContainer( + headerTitle: string, + lines: string[], + shown: number, + total: number, +) { const header = `### ${headerTitle} - showing ${shown} of ${total}`; const container = new ContainerBuilder() .addTextDisplayComponents( new TextDisplayBuilder().setContent(`${header}\n\n${lines.join("\n\n")}`), ) - .addSeparatorComponents(new SeparatorBuilder().setDivider(false).setSpacing(1)); + .addSeparatorComponents( + new SeparatorBuilder().setDivider(false).setSpacing(1), + ); return container; }

@@ -210,7 +230,9 @@ const showPrivate = Boolean(opts.showPrivate);

if (!bans || bans.length === 0) { const c = new ContainerBuilder().addTextDisplayComponents( - new TextDisplayBuilder().setContent("### Bans\n-# No restriction logs found"), + new TextDisplayBuilder().setContent( + "### Bans\n-# No restriction logs found", + ), ); return { container: c, total: 0 }; }

@@ -219,9 +241,16 @@ const enriched = enrichLogs(bans);

const sorted = enriched.sort((a, b) => b.startMs - a.startMs); const shown = sorted.slice(0, limit); - const lines = await Promise.all(shown.map((l) => formatRestrictionLog(l, { showPrivate }))); + const lines = await Promise.all( + shown.map((l) => formatRestrictionLog(l, { showPrivate })), + ); - const container = buildLogsContainer("Bans", lines, shown.length, bans.length); + const container = buildLogsContainer( + "Bans", + lines, + shown.length, + bans.length, + ); return { container, total: bans.length }; }

@@ -239,7 +268,9 @@ (log) =>

({ ...log, universeId, - project: Object.values(config.projects).find((p) => p.universe === universeId), + project: Object.values(config.projects).find( + (p) => p.universe === universeId, + ), }) as EnrichedLog, ); }),

@@ -274,7 +305,10 @@ return { container: c, total: 0 };

} const lastBans = allLogs - .sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()) + .sort( + (a, b) => + new Date(b.createTime).getTime() - new Date(a.createTime).getTime(), + ) .slice(0, 3); const projectForUniverse = (universeId: string) =>

@@ -286,13 +320,19 @@ const lines = await Promise.all(

enriched.map((log) => formatRestrictionLog(log, { showPrivate: opts.showPrivate, - includeProjectLabel: projectForUniverse(log.universeId || "")?.displayName || null, + includeProjectLabel: + projectForUniverse(log.universeId || "")?.displayName || null, includeStatus: true, }), ), ); - const container = buildLogsContainer("Recent Bans", lines, lines.length, allLogs.length); + const container = buildLogsContainer( + "Recent Bans", + lines, + lines.length, + allLogs.length, + ); return { container, total: allLogs.length }; }

@@ -307,7 +347,10 @@ originCommand?: string;

}, ) { const lastBans = extras?.bans - ?.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()) + ?.sort( + (a, b) => + new Date(b.createTime).getTime() - new Date(a.createTime).getTime(), + ) .slice(0, 3); const banSummary =

@@ -328,7 +371,9 @@ `@${profile.name} - ${profile.id} - created ${createdAtString}`,

].join("\n"), ); const aboutText = text( - ["### About", profile.about ? profile.about : "-# No bio provided"].join("\n"), + ["### About", profile.about ? profile.about : "-# No bio provided"].join( + "\n", + ), ); const banText = text([`-# ${banSummary}`].join("\n"));

@@ -352,7 +397,10 @@ .setStyle(ButtonStyle.Primary)

.setCustomId(`ban-history:summary:${profile.id}`); mainContainer.addActionRowComponents( - new ActionRowBuilder<ButtonBuilder>().addComponents(profileButton, bansButton), + new ActionRowBuilder<ButtonBuilder>().addComponents( + profileButton, + bansButton, + ), ); return { mainContainer, buttons: [profileButton] };
M src/roblox/robloxUser.tssrc/roblox/robloxUser.ts

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

import { isApiErrorV2, roblox } from "@/roblox/client"; import { constructUserContainer, getBans } from "@/roblox/profile"; import type { RobloxUserId } from "@/roblox/types"; -import type { ChatInputCommandInteraction, MessageContextMenuCommandInteraction } from "discord.js"; +import type { + ChatInputCommandInteraction, + MessageContextMenuCommandInteraction, +} from "discord.js"; export async function processRobloxUserInteraction( - interaction: ChatInputCommandInteraction | MessageContextMenuCommandInteraction, + interaction: + | ChatInputCommandInteraction + | MessageContextMenuCommandInteraction, id: RobloxUserId, isModerator: boolean, ) {

@@ -13,7 +18,9 @@ await interaction.editReply(`Fetching user with ID ${id}...`);

const [userRes, userErr] = await roblox.getUser(id); if (userErr) { if (isApiErrorV2(userErr)) { - await interaction.editReply(`There was an error fetching the user: ${userErr.message}`); + await interaction.editReply( + `There was an error fetching the user: ${userErr.message}`, + ); } else { await interaction.editReply("There was an error fetching the user."); }

@@ -21,7 +28,9 @@ return;

} const user = userRes; - await interaction.editReply(`Found user with username ${user.name}, fetching thumbnail...`); + await interaction.editReply( + `Found user with username ${user.name}, fetching thumbnail...`, + ); const [thumbnailRes] = await roblox.getThumbnail(id, { shape: "SQUARE" }); const thumbnailUri = thumbnailRes ? thumbnailRes.response?.imageUri : null;

@@ -33,7 +42,8 @@ "<:wawa:1158423817698430977>",

"<:max:1476776753845370991>", "<:snickerdoodle:1477173429575749716>", ]; - const message = randomMessages[Math.floor(Math.random() * randomMessages.length)]; + const message = + randomMessages[Math.floor(Math.random() * randomMessages.length)]; await interaction.editReply(message); const bans = await getBans(id);

@@ -52,6 +62,8 @@ components: [mainContainer],

}); } catch (err) { console.error(err); - await interaction.editReply("An unexpected error occurred while fetching the user."); + await interaction.editReply( + "An unexpected error occurred while fetching the user.", + ); } }
M src/roblox/types/index.tssrc/roblox/types/index.ts

@@ -88,7 +88,12 @@ traceId?: string;

}; } -export type ApiError = ApiErrorV2 | ApiErrorV1 | ApiErrorV1Alt | GatewayError | ApiValidationError; +export type ApiError = + | ApiErrorV2 + | ApiErrorV1 + | ApiErrorV1Alt + | GatewayError + | ApiValidationError; export type Result<T> = [T, null] | [null, ApiError];
M src/server/discord.tssrc/server/discord.ts

@@ -1,4 +1,4 @@

-import { type APIGuildMember, type APIUser } from "discord-api-types/v10"; +import type { APIGuildMember, APIUser } from "discord-api-types/v10"; import type { GuildMember, User } from "discord.js"; import config from "@/config";

@@ -39,13 +39,16 @@ mute: member.voice.serverMute || false,

flags: member.flags.bitfield, pending: member.pending, premium_since: member.premiumSince?.toISOString() || null, - communication_disabled_until: member.communicationDisabledUntil?.toISOString() || null, + communication_disabled_until: + member.communicationDisabledUntil?.toISOString() || null, } as APIGuildMember; } export async function getUsers(ids: string[]): Promise<void> { const uniqueIds = [...new Set(ids.filter(Boolean))]; - const missingIds = uniqueIds.filter((id) => id !== "system" && !cache.has(id)); + const missingIds = uniqueIds.filter( + (id) => id !== "system" && !cache.has(id), + ); if (missingIds.length === 0) return;

@@ -88,7 +91,8 @@ for (const ticket of allTickets) {

if (ticket.authorId) allIds.add(ticket.authorId); if (ticket.openedBy) allIds.add(ticket.openedBy); if (ticket.claimedBy) allIds.add(ticket.claimedBy); - if (ticket.closedBy && ticket.closedBy !== "reporter") allIds.add(ticket.closedBy); + if (ticket.closedBy && ticket.closedBy !== "reporter") + allIds.add(ticket.closedBy); } await getUsers(Array.from(allIds));

@@ -98,6 +102,9 @@ ...ticket,

authorId: ticket.authorId ? cache.get(ticket.authorId) : null, openedBy: ticket.openedBy ? cache.get(ticket.openedBy) : null, claimedBy: ticket.claimedBy ? cache.get(ticket.claimedBy) : null, - closedBy: ticket.closedBy && ticket.closedBy !== "reporter" ? cache.get(ticket.closedBy) : null, + closedBy: + ticket.closedBy && ticket.closedBy !== "reporter" + ? cache.get(ticket.closedBy) + : null, })); }
M src/server/index.tssrc/server/index.ts

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

-import { desc, eq, asc, and, isNotNull, ne, gte, inArray, or } from "drizzle-orm"; +import { + desc, + eq, + asc, + and, + isNotNull, + ne, + gte, + inArray, + or, +} from "drizzle-orm"; import type { BunRequest } from "bun"; import _logger from "@/utils/logging"; import config, { isProd } from "@/config"; -import { db, tickets, ticketMessages, moderators, sessions, attachments } from "@/database"; +import { + db, + tickets, + ticketMessages, + moderators, + sessions, + attachments, +} from "@/database"; import { getUser, getUsers, hydrateTickets } from "./discord"; const logger = _logger.child({ name: "server" });

@@ -18,11 +35,17 @@

async function getSessionUser(req: BunRequest) { const cookieHeader = req.headers.get("Cookie"); if (!cookieHeader) return null; - const sessionCookie = cookieHeader.split("; ").find((c) => c.startsWith("session=")); + const sessionCookie = cookieHeader + .split("; ") + .find((c) => c.startsWith("session=")); if (!sessionCookie) return null; const sessionId = sessionCookie.split("=")[1]; - const sessionList = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1); + const sessionList = await db + .select() + .from(sessions) + .where(eq(sessions.id, sessionId)) + .limit(1); if (sessionList.length === 0) return null; return { id: sessionList[0].userId, username: sessionList[0].username };

@@ -47,7 +70,10 @@ GET: async (req) => {

const url = new URL(req.url); const code = url.searchParams.get("code"); if (!code) - return new Response("No code provided", { status: 400, statusText: "No code provided" }); + return new Response("No code provided", { + status: 400, + statusText: "No code provided", + }); const params = new URLSearchParams(); params.append("client_id", CLIENT_ID);

@@ -56,17 +82,23 @@ params.append("grant_type", "authorization_code");

params.append("code", code); params.append("redirect_uri", REDIRECT_URI); - const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { - method: "POST", - body: params, - headers: { - "Content-Type": "application/x-www-form-urlencoded", + const tokenResponse = await fetch( + "https://discord.com/api/oauth2/token", + { + method: "POST", + body: params, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, }, - }); + ); const data = await tokenResponse.json(); if (data.error) - return new Response(data.error_description, { status: 400, statusText: data.error }); + return new Response(data.error_description, { + status: 400, + statusText: data.error, + }); const accessToken = data.access_token; const userResponse = await fetch("https://discord.com/api/users/@me", {

@@ -115,7 +147,11 @@ },

"/api/tickets": { GET: async (req: BunRequest) => { const user = await getSessionUser(req); - if (!user) return new Response(null, { status: 302, headers: { Location: "/login" } }); + if (!user) + return new Response(null, { + status: 302, + headers: { Location: "/login" }, + }); const modCheck = await db .select()

@@ -123,14 +159,17 @@ .from(moderators)

.where(eq(moderators.user_id, user.id)) .limit(1); if (modCheck.length === 0) { - return new Response("Forbidden: You must be a moderator to view tickets.", { - status: 403, - }); + return new Response( + "Forbidden: You must be a moderator to view tickets.", + { + status: 403, + }, + ); } const url = new URL(req.url); const limitStr = url.searchParams.get("limit") || "20"; - const limit = parseInt(limitStr, 10); + const limit = Number.parseInt(limitStr, 10); const allTickets = await db .select()

@@ -148,7 +187,11 @@ },

"/api/tickets/:id": { GET: async (req: BunRequest) => { const user = await getSessionUser(req); - if (!user) return new Response(null, { status: 302, headers: { Location: "/login" } }); + if (!user) + return new Response(null, { + status: 302, + headers: { Location: "/login" }, + }); const modCheck = await db .select()

@@ -156,15 +199,22 @@ .from(moderators)

.where(eq(moderators.user_id, user.id)) .limit(1); if (modCheck.length === 0) - return new Response("Forbidden: You must be a moderator to view tickets.", { - status: 403, - }); + return new Response( + "Forbidden: You must be a moderator to view tickets.", + { + status: 403, + }, + ); const { id } = req.params; - const ticketId = parseInt(id, 10); + const ticketId = Number.parseInt(id, 10); if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); - const ticketList = await db.select().from(tickets).where(eq(tickets.id, ticketId)).limit(1); + const ticketList = await db + .select() + .from(tickets) + .where(eq(tickets.id, ticketId)) + .limit(1); const _ticket = ticketList[0]; if (!_ticket) return new Response("Ticket not found", { status: 404 });

@@ -198,7 +248,10 @@ .select()

.from(attachments) .where( or( - and(eq(attachments.ownerType, "ticket"), eq(attachments.ownerId, ticketId)), + and( + eq(attachments.ownerType, "ticket"), + eq(attachments.ownerId, ticketId), + ), messageIds.length > 0 ? and( eq(attachments.ownerType, "ticket_message"),

@@ -212,7 +265,8 @@ const newMessages = await Promise.all(

messages.map(async (message) => { const author = await getUser(message.authorId); const msgAttachments = ticketAttachments.filter( - (a) => a.ownerType === "ticket_message" && a.ownerId === message.id, + (a) => + a.ownerType === "ticket_message" && a.ownerId === message.id, ); const mentions: Record<string, any> = {};

@@ -224,7 +278,12 @@ mentions[id] = await getUser(id);

} } - return { ...message, author, attachments: msgAttachments, mentions }; + return { + ...message, + author, + attachments: msgAttachments, + mentions, + }; }), );

@@ -252,16 +311,23 @@ .from(moderators)

.where(eq(moderators.user_id, user.id)) .limit(1); if (modCheck.length === 0) - return new Response("Forbidden: You must be a moderator to delete tickets.", { - status: 403, - }); + return new Response( + "Forbidden: You must be a moderator to delete tickets.", + { + status: 403, + }, + ); const { id } = req.params; - const ticketId = parseInt(id, 10); + const ticketId = Number.parseInt(id, 10); if (isNaN(ticketId)) return new Response("Invalid ID", { status: 400 }); - const deleted = await db.delete(tickets).where(eq(tickets.id, ticketId)).returning(); - if (deleted.length === 0) return new Response("Ticket not found", { status: 404 }); + const deleted = await db + .delete(tickets) + .where(eq(tickets.id, ticketId)) + .returning(); + if (deleted.length === 0) + return new Response("Ticket not found", { status: 404 }); return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" },

@@ -278,7 +344,8 @@ .select()

.from(moderators) .where(eq(moderators.user_id, user.id)) .limit(1); - if (modCheck.length === 0) return new Response("Forbidden", { status: 403 }); + if (modCheck.length === 0) + return new Response("Forbidden", { status: 403 }); const mods = await db.select().from(moderators); const ids = mods.map((mod) => mod.user_id);

@@ -307,13 +374,16 @@ .select()

.from(moderators) .where(eq(moderators.user_id, user.id)) .limit(1); - if (modCheck.length === 0) return new Response("Forbidden", { status: 403 }); + if (modCheck.length === 0) + return new Response("Forbidden", { status: 403 }); const url = new URL(req.url); const daysStr = url.searchParams.get("days"); - const days = daysStr ? parseInt(daysStr, 10) : null; + const days = daysStr ? Number.parseInt(daysStr, 10) : null; const guildId = url.searchParams.get("guildId"); - const cutoffDate = days ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) : null; + const cutoffDate = days + ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) + : null; const closedTickets = await db .select()

@@ -340,7 +410,9 @@ const mean = sum / durations.length;

const sorted = [...durations].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); const median = - sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; + sorted.length % 2 !== 0 + ? sorted[mid] + : (sorted[mid - 1] + sorted[mid]) / 2; return { mean, median, count: durations.length }; };

@@ -350,7 +422,9 @@ const hourlyDurations = new Map<number, number[]>();

for (const t of closedTickets) { const hour = t.closedAt!.getHours(); if (!hourlyDurations.has(hour)) hourlyDurations.set(hour, []); - hourlyDurations.get(hour)?.push(t.closedAt!.getTime() - t.createdAt.getTime()); + hourlyDurations + .get(hour) + ?.push(t.closedAt!.getTime() - t.createdAt.getTime()); } const hourly = [];

@@ -376,14 +450,17 @@ .select()

.from(moderators) .where(eq(moderators.user_id, user.id)) .limit(1); - if (modCheck.length === 0) return new Response("Forbidden", { status: 403 }); + if (modCheck.length === 0) + return new Response("Forbidden", { status: 403 }); const url = new URL(req.url); const daysStr = url.searchParams.get("days"); - const days = daysStr ? parseInt(daysStr, 10) : null; + const days = daysStr ? Number.parseInt(daysStr, 10) : null; const guildId = url.searchParams.get("guildId"); const orderBy = url.searchParams.get("order_by") || "median"; - const cutoffDate = days ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) : null; + const cutoffDate = days + ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) + : null; const closedTickets = await db .select()

@@ -404,7 +481,9 @@ for (const t of closedTickets) {

const modId = t.closedBy; if (!modId) continue; if (!modDurations.has(modId)) modDurations.set(modId, []); - modDurations.get(modId)?.push(t.closedAt!.getTime() - t.createdAt.getTime()); + modDurations + .get(modId) + ?.push(t.closedAt!.getTime() - t.createdAt.getTime()); } const getStats = (durations: number[]) => {

@@ -414,17 +493,22 @@ const mean = sum / durations.length;

const sorted = [...durations].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); const median = - sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; + sorted.length % 2 !== 0 + ? sorted[mid] + : (sorted[mid - 1] + sorted[mid]) / 2; return { mean, median, count: durations.length }; }; - const modStats = Array.from(modDurations.entries()).map(([modId, durations]) => { - const stats = getStats(durations); - return { modId, ...stats }; - }); + const modStats = Array.from(modDurations.entries()).map( + ([modId, durations]) => { + const stats = getStats(durations); + return { modId, ...stats }; + }, + ); if (orderBy === "mean") modStats.sort((a, b) => a.mean - b.mean); - else if (orderBy === "count") modStats.sort((a, b) => b.count - a.count); + else if (orderBy === "count") + modStats.sort((a, b) => b.count - a.count); else modStats.sort((a, b) => a.median - b.median); const idsToFetch = modStats.map((mod) => mod.modId);
M src/tasks/ticketWatcher.tssrc/tasks/ticketWatcher.ts

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

import { db, ticketMessages, tickets } from "@/database"; import { getGuild } from "@/database/queries"; import { loggers } from "@/utils/logging"; -import { type Client } from "discord.js"; +import type { Client } from "discord.js"; import { and, desc, eq } from "drizzle-orm"; const logger = loggers.events.child({ name: "ticketWatcher" });

@@ -20,7 +20,9 @@ let running = false;

const run = async () => { if (running) { - logger.warn("previous ticket watcher run still in progress, skipping this tick"); + logger.warn( + "previous ticket watcher run still in progress, skipping this tick", + ); return; } running = true;

@@ -69,7 +71,10 @@ .orderBy(desc(ticketMessages.createdAt))

.limit(1) .execute(); - if (lastUserMsg && new Date(lastUserMsg.createdAt).getTime() > lastModActivity) { + if ( + lastUserMsg && + new Date(lastUserMsg.createdAt).getTime() > lastModActivity + ) { await db .update(tickets) .set({

@@ -85,7 +90,9 @@ }

} } - const createdAt = ticket.createdAt ? new Date(ticket.createdAt).getTime() : null; + const createdAt = ticket.createdAt + ? new Date(ticket.createdAt).getTime() + : null; if (!createdAt) continue; const age = Date.now() - createdAt;

@@ -94,7 +101,10 @@ const userMsgs = await db

.select() .from(ticketMessages) .where( - and(eq(ticketMessages.ticketId, ticket.id), eq(ticketMessages.authorType, "user")), + and( + eq(ticketMessages.ticketId, ticket.id), + eq(ticketMessages.authorType, "user"), + ), ) .execute();

@@ -104,7 +114,9 @@

if (!ticket.warnedAt && age >= WARN_MS && age < CLOSE_MS) { if (ticket.channelId && ticket.guildId) { try { - const channel = await client.channels.fetch(ticket.channelId).catch(() => null); + const channel = await client.channels + .fetch(ticket.channelId) + .catch(() => null); if (channel?.isSendable()) { await channel.send( `<@${ticket.authorId}>, this ticket will be automatically archived in 15 minutes due to inactivity. Please send a message in this channel to keep it open.`,

@@ -139,15 +151,21 @@ .execute();

if (ticket.channelId && ticket.guildId) { try { - const channel = await client.channels.fetch(ticket.channelId).catch(() => null); + const channel = await client.channels + .fetch(ticket.channelId) + .catch(() => null); if (!channel) continue; - await channel.delete(`Auto-archived due to inactivity for ticket ${ticket.id}`); + await channel.delete( + `Auto-archived due to inactivity for ticket ${ticket.id}`, + ); const authorId = ticket.authorId; if (authorId) { - const user = await client.users.fetch(authorId).catch(() => null); + const user = await client.users + .fetch(authorId) + .catch(() => null); if (user) { await user .send(

@@ -157,7 +175,9 @@ .catch(() => null);

} } - const { data: guildData, exists: guildExists } = await getGuild(ticket.guildId); + const { data: guildData, exists: guildExists } = await getGuild( + ticket.guildId, + ); if (!guildExists) continue; const transcriptChannelId = guildData?.ticket_channel_id;

@@ -173,11 +193,16 @@ await transcriptChannel.send({

content: `Ticket #${ticket.id} has been archived due to inactivity.`, }); } catch (err) { - logger.error(err, `failed to delete channel for ticket ${ticket.id}`); + logger.error( + err, + `failed to delete channel for ticket ${ticket.id}`, + ); } } - logger.info(`archived and deleted ticket ${ticket.id} due to inactivity`); + logger.info( + `archived and deleted ticket ${ticket.id} due to inactivity`, + ); } } catch (errTicket) { logger.error(errTicket, `error processing ticket ${ticket.id}`);
M src/types.tssrc/types.ts

@@ -20,7 +20,16 @@ execute?: (client: Client, interaction: BaseInteraction) => Promise<void>;

enabled?: boolean; autocomplete?: (interaction: AutocompleteInteraction) => Promise<void>; - buttonExecute?: (client: Client, interaction: ButtonInteraction) => Promise<void>; - modalExecute?: (client: Client, interaction: ModalSubmitInteraction) => Promise<void>; - selectMenuExecute?: (client: Client, interaction: StringSelectMenuInteraction) => Promise<void>; + buttonExecute?: ( + client: Client, + interaction: ButtonInteraction, + ) => Promise<void>; + modalExecute?: ( + client: Client, + interaction: ModalSubmitInteraction, + ) => Promise<void>; + selectMenuExecute?: ( + client: Client, + interaction: StringSelectMenuInteraction, + ) => Promise<void>; }
M src/utils/discord/components.tssrc/utils/discord/components.ts

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

import { TextDisplayBuilder, ThumbnailBuilder } from "discord.js"; -export function makeComponentId(cmd: unknown, ...parts: Array<string | number>): string { +export function makeComponentId( + cmd: unknown, + ...parts: Array<string | number> +): string { const name = typeof cmd === "string" ? cmd
M src/utils/discord/permissions.tssrc/utils/discord/permissions.ts

@@ -1,8 +1,14 @@

import type { GuildMember } from "discord.js"; import { getGuild, getManagerRoleIds } from "../../database/queries"; -export async function hasManagerPermissions(member: GuildMember): Promise<boolean> { - if (member.permissions.has("ManageGuild") || member.guild.ownerId === member.id) return true; +export async function hasManagerPermissions( + member: GuildMember, +): Promise<boolean> { + if ( + member.permissions.has("ManageGuild") || + member.guild.ownerId === member.id + ) + return true; const guild = await getGuild(member.guild.id); const managers = await getManagerRoleIds(guild.data.guildId);
M src/utils/logging.tssrc/utils/logging.ts

@@ -1,7 +1,11 @@

import { pino } from "pino"; const isProd = Bun.env.NODE_ENV === "production"; -const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isProd ? "info" : "debug"; +const logLevel = process.env.LOG_LEVEL + ? process.env.LOG_LEVEL + : isProd + ? "info" + : "debug"; const logger = pino({ transport: {
M src/utils/tickets/formatting.tssrc/utils/tickets/formatting.ts

@@ -5,5 +5,8 @@ const templates: Record<string, string> = {

"{user}": `<@${user}>`, }; - return Object.entries(templates).reduce((msg, [key, val]) => msg.split(key).join(val), message); + return Object.entries(templates).reduce( + (msg, [key, val]) => msg.split(key).join(val), + message, + ); }
M src/utils/tickets/transcripts.tssrc/utils/tickets/transcripts.ts

@@ -1,8 +1,16 @@

-import { type Attachment, type Ticket, attachments, db, ticketMessages } from "@/database"; +import { + type Attachment, + type Ticket, + attachments, + db, + ticketMessages, +} from "@/database"; import { getPublicUrl } from "@/utils/s3"; import { and, asc, eq } from "drizzle-orm"; -export async function generateTranscript(ticket: Ticket): Promise<[string, Attachment[]]> { +export async function generateTranscript( + ticket: Ticket, +): Promise<[string, Attachment[]]> { const messages = await db .select() .from(ticketMessages)

@@ -16,7 +24,9 @@ .select()

.from(attachments) .where(and(eq(attachments.ownerType, "ticket_message"))); - const relevantAttachments = ticketAttachments.filter((att) => messageIds.includes(att.ownerId)); + const relevantAttachments = ticketAttachments.filter((att) => + messageIds.includes(att.ownerId), + ); const attachmentMap = new Map<number, Attachment[]>(); for (const att of relevantAttachments) {
M src/utils/uwuify.tssrc/utils/uwuify.ts

@@ -4,7 +4,13 @@ export function uwuify(input: string) {

const uwuifier = new Uwuifier(); const uwuifiedContent = input ? uwuifier.uwuifySentence(input) : ""; - const blacklistedWords = ["twerks", "sees bulge", "notices buldge", "starts twerking", "wank"]; + const blacklistedWords = [ + "twerks", + "sees bulge", + "notices buldge", + "starts twerking", + "wank", + ]; return blacklistedWords.reduce((acc, word) => { return acc.replace(new RegExp(word, "gi"), "----");
M src/utils/woke.tssrc/utils/woke.ts

@@ -24,7 +24,11 @@ reflexive: "themself",

}; export function getModerator(id: string): Moderator { - const moderator = db.select().from(moderators).where(eq(moderators.user_id, id)).get(); + const moderator = db + .select() + .from(moderators) + .where(eq(moderators.user_id, id)) + .get(); if (!moderator) return { id, ...defaultModerator }; return { id: id, ...moderator }; }