Compare commits

..

10 Commits

31 changed files with 1670 additions and 817 deletions

16
.dockerignore Normal file
View File

@@ -0,0 +1,16 @@
.dockerignore
.editorconfig
.env
.git
.gitignore
.idea
.vscode
coverage*
docker-compose*
Dockerfile*
helm-charts
Justfile
LICENSE
Makefile
node_modules
README.md

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules
.env
config.json
build

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
}

37
Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lock /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# [optional] tests & build
ENV NODE_ENV=production
# RUN bun test
RUN bun build --target bun --outdir build index.ts
# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/build/index.js .
# run the app
USER bun
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "index.js" ]

18
Justfile Normal file
View File

@@ -0,0 +1,18 @@
fmt:
just format
format:
bunx --bun @biomejs/biome format --write *.ts plugins/
lint:
bunx --bun @biomejs/biome lint --fix *.ts plugins/
build:
bun build --target bun --outdir build index.ts
docker-build:
docker build --pull -t blitzcrank:latest-dev .
docker-run:
docker run --env-file .env blitzcrank:latest-dev

12
biome.jsonc Normal file
View File

@@ -0,0 +1,12 @@
{
"javascript": {
// This is taken from the Google TS style guide:
// https://github.com/google/gts/blob/main/.prettierrc.json
"formatter": {
"bracketSpacing": false,
"quoteStyle": "single",
"trailingCommas": "all",
"arrowParentheses": "asNeeded"
}
}
}

192
bun.lock
View File

@@ -7,9 +7,13 @@
"@biomejs/biome": "^2.0.6",
"chrono-node": "^2.8.3",
"discord.js": "^14.21.0",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.7",
"sqlite3": "^5.1.7",
},
"devDependencies": {
"vitest": "^3.2.4",
},
},
},
"packages": {
@@ -43,12 +47,104 @@
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
"@npmcli/fs": ["@npmcli/fs@1.1.1", "", { "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" } }, "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ=="],
"@npmcli/move-file": ["@npmcli/move-file@1.1.2", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.2", "", { "os": "android", "cpu": "arm" }, "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.2", "", { "os": "android", "cpu": "arm64" }, "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.2", "", { "os": "linux", "cpu": "arm" }, "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.2", "", { "os": "linux", "cpu": "none" }, "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.2", "", { "os": "linux", "cpu": "none" }, "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.2", "", { "os": "linux", "cpu": "none" }, "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.2", "", { "os": "linux", "cpu": "x64" }, "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.2", "", { "os": "win32", "cpu": "x64" }, "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA=="],
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
@@ -57,8 +153,14 @@
"@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="],
@@ -67,6 +169,20 @@
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
"@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
"abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
@@ -83,6 +199,8 @@
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
@@ -95,8 +213,14 @@
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"cacache": ["cacache@15.3.0", "", { "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "glob": "^7.1.4", "infer-owner": "^1.0.4", "lru-cache": "^6.0.0", "minipass": "^3.1.1", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.2", "mkdirp": "^1.0.3", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^8.0.1", "tar": "^6.0.2", "unique-filename": "^1.1.1" } }, "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ=="],
"chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="],
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"chrono-node": ["chrono-node@2.8.3", "", { "dependencies": { "dayjs": "^1.10.0" } }, "sha512-YukiXak31pshonVWaeJ9cZ4xxWIlbsyn5qYUkG5pQ+usZ6l22ASXDIk0kHUQkIBNOCLRevFkHJjnGKXwZNtyZw=="],
@@ -115,6 +239,8 @@
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
@@ -137,10 +263,20 @@
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
@@ -149,6 +285,8 @@
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="],
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
@@ -193,16 +331,22 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
"loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="],
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"make-fetch-happen": ["make-fetch-happen@9.1.0", "", { "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^6.0.0", "minipass": "^3.1.3", "minipass-collect": "^1.0.2", "minipass-fetch": "^1.3.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.2", "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" } }, "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
@@ -235,6 +379,8 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
@@ -255,8 +401,20 @@
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
"pg-hstore": ["pg-hstore@2.3.4", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
@@ -275,6 +433,8 @@
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"rollup": ["rollup@4.44.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.2", "@rollup/rollup-android-arm64": "4.44.2", "@rollup/rollup-darwin-arm64": "4.44.2", "@rollup/rollup-darwin-x64": "4.44.2", "@rollup/rollup-freebsd-arm64": "4.44.2", "@rollup/rollup-freebsd-x64": "4.44.2", "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", "@rollup/rollup-linux-arm-musleabihf": "4.44.2", "@rollup/rollup-linux-arm64-gnu": "4.44.2", "@rollup/rollup-linux-arm64-musl": "4.44.2", "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", "@rollup/rollup-linux-riscv64-gnu": "4.44.2", "@rollup/rollup-linux-riscv64-musl": "4.44.2", "@rollup/rollup-linux-s390x-gnu": "4.44.2", "@rollup/rollup-linux-x64-gnu": "4.44.2", "@rollup/rollup-linux-x64-musl": "4.44.2", "@rollup/rollup-win32-arm64-msvc": "4.44.2", "@rollup/rollup-win32-ia32-msvc": "4.44.2", "@rollup/rollup-win32-x64-msvc": "4.44.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
@@ -287,6 +447,8 @@
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
@@ -299,12 +461,18 @@
"socks-proxy-agent": ["socks-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"sqlite3": ["sqlite3@5.1.7", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { "node-gyp": "8.x" } }, "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog=="],
"ssri": ["ssri@8.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
@@ -313,12 +481,26 @@
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="],
"toposort-class": ["toposort-class@1.0.1", "", {}, "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg=="],
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
@@ -327,6 +509,8 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"underscore": ["underscore@1.13.7", "", {}, "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="],
"undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
@@ -341,8 +525,16 @@
"validator": ["validator@13.15.15", "", {}, "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="],
"vite": ["vite@7.0.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
"wkx": ["wkx@0.5.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg=="],

View File

@@ -1,26 +0,0 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
import { Settings } from "./common";
import { Nag, CheckIn } from './common';
const data = new SlashCommandBuilder()
.setName("checkin")
.setDescription("Check-in for your daily nag")
.addStringOption((option) =>
option
.setName("text")
.setDescription("Optional description of what you have achieved"),
);
async function initialize(settings: Settings) {}
function execute(interaction: ChatInputCommandInteraction) {
}
export default function () {
return {
data,
};
}

View File

@@ -1,133 +0,0 @@
import {
ChatInputCommandInteraction,
Client,
SlashCommandBuilder,
TextChannel,
} from "discord.js";
import {
Sequelize,
Model,
INTEGER,
STRING,
BOOLEAN,
DATE,
literal,
} from "sequelize";
export interface Settings {
client: Client; // Main Discord client object
db: Sequelize; // Database access object
publicChannel?: string; // Channel to use if a reminder is public
loopIntervalSec?: number; // Loop interval in seconds
}
export class Nag extends Model {
// Primary key
declare id: number;
// User who created this nag
declare userId: number;
// Description of what you're supposed to do
declare text: string;
// Custom failure text
declare failText?: string;
// Should we @here?
declare mentionHere?: boolean;
}
export class CheckIn extends Model {
declare nagId: string;
// Date of the last time user ran /checkin
declare lastCheckIn: Date;
}
export async function initAndSyncTables(sequelize: Sequelize) {
Nag.init(
{
id: {
primaryKey: true,
type: INTEGER,
autoIncrement: true,
},
userId: {
type: INTEGER,
allowNull: false,
},
text: {
type: STRING,
allowNull: false,
},
failText: {
type: STRING,
},
mentionHere: {
type: BOOLEAN,
},
},
{ sequelize },
);
CheckIn.init(
{
nagId: {
type: INTEGER,
allowNull: false,
},
lastCheckIn: {
type: DATE,
allowNull: false,
},
},
{ sequelize },
);
Nag.hasOne(CheckIn, { foreignKey: "nagId" });
await Nag.sync();
await CheckIn.sync();
}
import { Guild, Channel } from "discord.js";
export class Plugin {
settings: Settings;
publicChannel?: Channel;
interval: NodeJS.Timeout;
constructor(settings: Settings) {
this.settings = settings;
}
start() {
if (!this.settings.loopIntervalSec) {
this.settings.loopIntervalSec = 60; // 1 minute
}
this.interval = setInterval(
this.loop,
this.settings.loopIntervalSec * 1000,
);
}
async triggerNag(nag: Nag) {
const client = this.settings.client;
const chan = client.channels.cache.get("1234"); // TODO
if (!(chan instanceof TextChannel)) {
return; // TODO
}
try {
const failText =
nag.failText ??
`<@${nag.userId}> didn't complete "${nag.text}". Shame shame!`;
const mentionHere = nag.mentionHere ? "<@here> " : "";
const msg = `${mentionHere}${failText}`;
await chan.send(msg);
} catch (error) {
console.log("Error while creating Nag:", error); // TODO
}
}
async loop() {
console.debug("nag.js main loop");
// Find all nags where the last check-in was before (next check in) - (24 hours)
}
stop() {
clearInterval(this.interval);
}
}

View File

@@ -1,79 +0,0 @@
import {
ChatInputCommandInteraction,
Client,
SlashCommandBuilder,
} from "discord.js";
import { Sequelize, literal } from "sequelize";
import { Nag, CheckIn, Settings } from "./common";
import { Chrono } from "chrono-node";
const data = new SlashCommandBuilder()
.setName("nag")
.setDescription("Let Blitzcrank nag you every day about something")
.addStringOption((option) =>
option
.setRequired(true)
.setName("text")
.setDescription("What you have to do every day"),
)
.addStringOption((option) =>
option
.setName("failText")
.setDescription("Custom message to be broadcast on failure"),
)
.addBooleanOption((option) =>
option
.setName("mentionHere")
.setDescription("Whether to DM you or @ a channel")
.setRequired(false),
);
function lateCheckedInUsers() {
return Nag.findAll({
include: [CheckIn],
where: literal(
"checkInTime <= datetime('now', '-1 day', 'start of day', '+9 hours')",
),
});
}
async function initialize(settings: Settings) {}
async function execute(interaction: ChatInputCommandInteraction) {
const text = interaction.options.getString("text");
if (text === null || text === undefined) {
await interaction.reply("Nag can't have a blank `text`, try again.");
return;
}
const nag = await Nag.create({
userId: interaction.user.id,
text: text,
failText: interaction.options.getString("failText"),
mentionHere: interaction.options.getBoolean("mentionHere") ?? false,
});
await nag.save();
const chrono = new Chrono();
const checkIn = chrono.parseDate("today at 9AM");
if (!checkIn) {
await interaction.reply(
"Internal error while saving your nag. Tell Drew the bot is broken!!!",
);
return;
}
await CheckIn.create({
nagId: nag.id,
checkIn: checkIn,
});
await interaction.reply(
`I'll check every day at 9AM if you've completed '${text}'. If not, I'll nag you! Use /checkin to prevent a shameful callout, and /unnag to cancel.`,
);
}
export default function (settings: Settings) {
return {
data,
execute,
initialize: async () => await initialize(settings),
};
}

View File

@@ -1,29 +0,0 @@
import {
ChatInputCommandInteraction,
Client,
SlashCommandBuilder,
} from "discord.js";
import { Sequelize } from "sequelize";
import { Settings } from './common'
const data = new SlashCommandBuilder()
.setName("unnag")
.setDescription("Remove a nag");
async function initialize(settings: Settings) {
}
async function execute(interaction: ChatInputCommandInteraction) {
return;
}
export default function (settings: Settings) {
return {
data,
execute,
initialize: async() => await initialize(settings),
};
}

View File

@@ -1,18 +0,0 @@
import { type Client, SlashCommandBuilder } from "discord.js";
import type { Sequelize } from "sequelize";
export default function (settings: { client: Client; db: Sequelize }) {
return {
data: new SlashCommandBuilder()
.setName("ping")
.setDescription("Send a ping to the bot"),
initialize: async () => {},
execute: async (interaction) => {
await interaction.reply(
`Pong! This command was run by ${interaction.user.username}, who joined on ${interaction.member.joinedAt}.`,
);
},
};
}

View File

@@ -1,229 +0,0 @@
// import type { ChatInputCommandInteraction } from "discord.js";
import {
PermissionsBitField,
SlashCommandBuilder,
TextChannel,
type ChatInputCommandInteraction,
type Client,
} from "discord.js";
import { type Sequelize, Model, INTEGER, TEXT, DATE, BOOLEAN } from "sequelize";
import * as sequelize from "sequelize";
import * as chrono from "chrono-node";
// const REMINDERS_CHANNEL = "1062196593379520593"; // #bot-test-channel
// const REMINDERS_CHANNEL = ""; // #general
interface Settings {
client: Client; // Main Discord client object
db: Sequelize; // Database access object
responseMode: string;
publicChannel: string; // Channel to use if a reminder is public
loopIntervalSec?: number; // Loop interval in seconds
}
const data = new SlashCommandBuilder()
.setName("remind")
.setDescription("Remind me to do something")
.addStringOption((option) =>
option
.setName("when")
.setDescription("Short description of when you want the reminder")
.setRequired(true),
)
.addStringOption((option) =>
option
.setName("what")
.setDescription("Short description of what you want to be reminded of")
.setRequired(true),
);
class Reminder extends Model {
declare id: number;
declare userId: string;
declare text: string | null;
declare trigger: Date;
declare isValid: boolean;
declare requestChannel: string;
declare requestMessage: string;
}
class Plugin {
settings: Settings;
interval: NodeJS.Timeout;
constructor(settings: Settings) {
this.settings = settings;
}
getChannel(reminder: Reminder) {
let channel: TextChannel | null = null;
const client = this.settings.client;
const publicChannel = this.settings.publicChannel;
// if (this.settings.responseMode === "private") {
// channel = getSendableTextChannel(client, reminder.requestChannel);
// }
if (this.settings.responseMode === "public" || !channel) {
channel = getSendableTextChannel(client, publicChannel);
}
if (!channel) {
throw Error("Cannot find valid channel to send reminder on");
}
return channel;
}
async loop() {
console.debug("remind.js main loop");
const results = await Reminder.findAll({
// NOTE: 'trigger' is the time when the reminder should go off. If trigger -
// now is positive, trigger is in the future. If trigger - now <=
// 1.0, then we have less than one minute before the timer should go
// off.
where: sequelize.literal(
"isValid AND (julianday(trigger) - julianday('now')) <= 1.0",
),
});
for (const reminder of results) {
try {
const now = new Date(Date.now());
const delay = reminder.trigger.getTime() - now.getTime();
console.log(
`Callback for Reminder ${reminder.get("id")} triggering in ${delay}ms`,
);
const channel = this.getChannel(reminder);
setTimeout(async () => await triggerReminder(channel, reminder), delay);
} catch (error) {
console.error(
`Error while processing Reminder ${reminder.id}: ${error}`,
);
console.trace();
}
}
}
start() {
this.interval = setInterval(
this.loop.bind(this),
1000 * (this.settings.loopIntervalSec ?? 60),
);
}
stop() {
clearInterval(this.interval);
}
}
async function initialize(settings: Settings) {
console.log("Initializing remind.js");
// Populate Reminder table inside SQLite DB
Reminder.init(
{
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
},
userId: {
type: TEXT,
allowNull: false,
},
text: TEXT,
trigger: {
type: DATE,
allowNull: false,
},
isValid: BOOLEAN,
requestChannel: TEXT,
requestMessage: TEXT,
},
{ sequelize: settings.db },
);
await Reminder.sync();
// Try and determine how the bot will send reminders
console.debug(`Accessing channel ${settings.publicChannel}`);
if (!getSendableTextChannel(settings.client, settings.publicChannel)) {
throw Error(
"Invalid value for publicChannel; specify a channel the bot can access.",
);
}
// Initialize the 'plugin' object which will store all state required to control the mainloop.
if (settings.loopIntervalSec == null || settings.loopIntervalSec <= 0) {
console.log("Setting plugin loop interval to default of 60 seconds");
settings.loopIntervalSec = 60;
}
const plugin = new Plugin(settings);
plugin.start();
}
function getSendableTextChannel(client: Client, chanId: string) {
// console.log(`getSendableTextChannel(${client}, ${chanId})`);
const channel = client.channels.cache.get(chanId);
if (!client.user) {
throw Error("Can't check non-existent client");
}
if (!channel) {
throw Error("No such channel is visible to Blitz");
}
if (!channel?.isSendable()) {
throw Error("Channel is not a text channel, or otherwise not Sendable");
}
if (!(channel instanceof TextChannel)) {
throw Error("Channel is not a guild channel");
}
const permissions = channel?.permissionsFor(client.user);
const requiredPerms = new PermissionsBitField([
PermissionsBitField.Flags.SendMessages,
PermissionsBitField.Flags.ViewChannel,
]);
if (!permissions?.has(requiredPerms)) {
throw Error("Missing required permissions: SendMessages, ViewChannel");
}
return channel;
}
async function triggerReminder(channel: TextChannel, reminder: Reminder) {
try {
await channel.send({
content: `Reminder for <@${reminder.userId}>: ${reminder.text}`,
});
reminder.isValid = false;
} catch (error) {
console.log(`Error trigggering Reminder ${reminder.id}: ${error}`);
} finally {
await reminder.save();
}
}
async function execute(interaction: ChatInputCommandInteraction) {
const whenString = interaction.options.getString("when") ?? "now";
const when = chrono.parseDate(whenString);
if (!when) {
await interaction.reply(`Sorry, I don't understand '${when}' as a date`);
return;
}
const reminder = await Reminder.create({
userId: interaction.user.id,
text: interaction.options.getString("what"),
trigger: when, // TODO
requestMessage: interaction.id,
requestChannel: interaction.channelId,
isValid: true,
});
reminder.save();
await interaction.reply(`Ok, I'll remind you at ${when}`);
console.info(
`Created Reminder ${reminder.id} to be triggered at ${reminder.trigger}`,
);
}
export default function (settings: Settings) {
return {
data,
execute,
initialize: async () => await initialize(settings),
Reminder,
settings,
};
}

View File

@@ -1,34 +0,0 @@
import {
SlashCommandBuilder,
TextChannel,
type ChatInputCommandInteraction,
type Client,
type InviteStageInstance,
} from "discord.js";
import type { Sequelize } from "sequelize";
import { quotes } from "./quotes.json";
async function execute(interaction: ChatInputCommandInteraction) {
try {
const index = Math.floor(Math.random() * quotes.length);
await interaction.reply?.({
content: `> *${quotes[index].quote}*
> — ${quotes[index].author}`,
});
} catch (error) {
console.error(`Problem sending to channel: ${error}`);
}
}
async function initialize() {}
export default function (settings: {}) {
return {
data: new SlashCommandBuilder()
.setName("quote")
.setDescription("Print a quote from League of Legends."),
initialize: initialize,
execute: execute,
};
}

View File

@@ -1,171 +0,0 @@
{
"quotes": [
{ "author": "Blitzcrank", "quote": "A rolling golem gathers no rust." },
{ "author": "Blitzcrank", "quote": "Fired up and ready to serve." },
{ "author": "Blitzcrank", "quote": "Metal is harder than flesh." },
{ "author": "Camille", "quote": "[Speaking to Warwick] We are all monsters. Now, you are just one on the outside." },
{ "author": "Camille", "quote": "Efficiency is paramount to success." },
{ "author": "Camille", "quote": "Elegance never goes out of fashion." },
{ "author": "Camille", "quote": "Extremes are easy, it's the balance that is difficult." },
{ "author": "Camille", "quote": "I don't play the game, I make the rules." },
{ "author": "Camille", "quote": "I'm what you would call a 'deniable asset'." },
{ "author": "Camille", "quote": "It is not the weapon that defines you, but how you wield it." },
{ "author": "Camille", "quote": "It's not lies that cut, but the sharpness of the truth." },
{ "author": "Camille", "quote": "Mediocrity is the root of all evil." },
{ "author": "Camille", "quote": "Morality is a beautiful servant and a dangerous master." },
{ "author": "Camille", "quote": "Precision is the difference between a butcher and a surgeon." },
{ "author": "Camille", "quote": "Privilege must be preserved at all costs." },
{ "author": "Camille", "quote": "Progress is honed on necessary death." },
{ "author": "Camille", "quote": "Progress is served by technology, not controlled by it." },
{ "author": "Camille", "quote": "Regret is what tempers the steel of our soul." },
{ "author": "Camille", "quote": "Results are all that matters." },
{ "author": "Camille", "quote": "Self-made women need to be more prevalent." },
{ "author": "Camille", "quote": "Sometimes scars are the most refined attire one can wear." },
{ "author": "Camille", "quote": "The right word cuts more deeply than a knife." },
{ "author": "Camille", "quote": "The task at hand is the only one that matters." },
{ "author": "Camille", "quote": "The world is not black or white, but a delicious shade of grey." },
{ "author": "Camille", "quote": "Violence is a means to an end." },
{ "author": "Ekko", "quote": "A second chance? I thought I was on my fifth!" },
{ "author": "Ekko", "quote": "Good a time as any to act reckless." },
{ "author": "Ekko", "quote": "It's not how much time you have, it's how you use it." },
{ "author": "Ekko", "quote": "Never had luck. Never needed it." },
{ "author": "Ekko", "quote": "Time doesn't heal all wounds." },
{ "author": "Heimerdinger", "quote": "42... there's just something about that number." },
{ "author": "Heimerdinger", "quote": "I prefer a battle of wits, but you're unarmed!" },
{ "author": "Heimerdinger", "quote": "Why do chemists call helium, curium, and barium 'the medical elements'? Because, if you can't 'helium' or 'curium', you 'barium'! Hm hm!" },
{ "author": "Heimerdinger", "quote": "ヽ༼ຈل͜ຈ༽ノ raise your dongers" },
{ "author": "Jhin", "quote": "Art must exist beyond reason." },
{ "author": "Jhin", "quote": "Four!" },
{ "author": "Jhin", "quote": "I cannot be good. I must be perfection." },
{ "author": "Jhin", "quote": "I swear each performance is the last, but I lie every time." },
{ "author": "Jhin", "quote": "In carnage, I bloom, like a flower in the dawn." },
{ "author": "Jhin", "quote": "It is by my will alone I set my mind in motion." },
{ "author": "Jhin", "quote": "It's fun to kill a man, to take all that he had, and could ever have." },
{ "author": "Jhin", "quote": "You will learn what beauty truly is." },
{ "author": "Jinx", "quote": "Fishbones, you know what we oughta do? 'Do the laundry, wash dishes and pay some bills.' Stupid dumb rocket launcher..." },
{ "author": "Jinx", "quote": "I'm crazy! Got a doctor's note." },
{ "author": "Jinx", "quote": "I'm trying to care! But I just... can't!" },
{ "author": "Jinx", "quote": "Rules are made to be broken... like buildings! Or people!" },
{ "author": "Joseph Miklos", "quote": "'It's only a short way'? Is that a short joke?!" },
{ "author": "Joseph Miklos", "quote": "I am evil! Stop laughing!" },
{ "author": "Joseph Miklos", "quote": "Know that if the tables were turned, I would show you no mercy!" },
{ "author": "Joseph Miklos", "quote": "You will die by my hand!" },
{ "author": "Mordekaiser", "quote": "Ah, life is a bitter shame." },
{ "author": "Mordekaiser", "quote": "All mortals reek with the stench of decaying flesh." },
{ "author": "Mordekaiser", "quote": "Fools fear death, the strong wield it." },
{ "author": "Mordekaiser", "quote": "I alone am the bastion between eternal existence and oblivion." },
{ "author": "Mordekaiser", "quote": "I carve my kingdom beyond, from the ashes of nothing, no mortals, not even gods, will stop me from claiming what is mine." },
{ "author": "Mordekaiser", "quote": "I have bent the realm of the dead to my will, this world shall be next." },
{ "author": "Mordekaiser", "quote": "I raise my iron fist to subjugate the living." },
{ "author": "Mordekaiser", "quote": "I will grind their petty souls into mortar." },
{ "author": "Mordekaiser", "quote": "I will silence the incessant thrum of mortal hearts." },
{ "author": "Mordekaiser", "quote": "In the world beyond, blackened ichor filled a crumbling sky, as souls withered to nothing. But I refused to fade." },
{ "author": "Mordekaiser", "quote": "Mortals plan in fear for tomorrow, I build for eternity." },
{ "author": "Mordekaiser", "quote": "Naive men pray to the gods; they will learn to pray to me." },
{ "author": "Mordekaiser", "quote": "Only the worthy receive the gift of Nightfall's kiss." },
{ "author": "Mordekaiser", "quote": "Shed the frailty of flesh, embrace the cold edge of iron." },
{ "author": "Mordekaiser", "quote": "The dead belong to me, the living shall be next." },
{ "author": "Mordekaiser", "quote": "The world has tried to forget my existence, time to remind them why they fear." },
{ "author": "Mordekaiser", "quote": "Twice slain, thrice born." },
{ "author": "Mordekaiser", "quote": "Weaklings cower in the light, I bring eternal darkness." },
{ "author": "Poppy", "quote": "I'm in, one-hundred-percent! That's everything, right?" },
{ "author": "Poppy", "quote": "I'm no hero—just a Yordle with a hammer." },
{ "author": "Poppy", "quote": "Just had a thought—three pigtails!" },
{ "author": "Poppy", "quote": "The hammer does most of the work, I just swing it." },
{ "author": "Rammus", "quote": "OK." },
{ "author": "Rammus", "quote": "🆗" },
{ "author": "Senna", "quote": "I forgive. No one else has to." },
{ "author": "Senna", "quote": "I remember my nightmares. Wish I could remember to dream." },
{ "author": "Sion", "quote": "A black eye for the earth!" },
{ "author": "Sion", "quote": "Death had its chance." },
{ "author": "Sion", "quote": "Noxus suffers no cowards." },
{ "author": "Sion", "quote": "The quiet... eats at me." },
{ "author": "Swain", "quote": "A calculated risk is no risk at all." },
{ "author": "Swain", "quote": "A new vantage, is all the advantage I need." },
{ "author": "Swain", "quote": "And to think, they called me a 'cripple'." },
{ "author": "Swain", "quote": "Destiny marches—like any man." },
{ "author": "Swain", "quote": "Diplomacy is a subtle art." },
{ "author": "Swain", "quote": "Hmph... I do so enjoy explaining things to idiots." },
{ "author": "Swain", "quote": "I cannot lead if I allow fools to stumble about before me." },
{ "author": "Swain", "quote": "I could kill them all. But it would be far crueler to show them that I am right." },
{ "author": "Swain", "quote": "I have killed more men with words than by my own hand. Not for lack of trying." },
{ "author": "Swain", "quote": "I suppose I should be grateful they have the decency to fear me." },
{ "author": "Swain", "quote": "I've heard what they call me. What a waste of their final words." },
{ "author": "Swain", "quote": "If they already call me a villain, what will they call me when I succeed?" },
{ "author": "Swain", "quote": "Is it not enough for Noxus to be strong?" },
{ "author": "Swain", "quote": "It is not the visions that haunt me—but what I do not see." },
{ "author": "Swain", "quote": "Never make a bargain with a demon... that you intend to keep." },
{ "author": "Swain", "quote": "One can read the future in battle lines, assuming one can read." },
{ "author": "Swain", "quote": "People often ask for a hero, when a villain is what they truly need." },
{ "author": "Swain", "quote": "Pity stays the hand of the merciful, but not mine." },
{ "author": "Swain", "quote": "Tell me again all the crimes I've committed, and I'll tell you the price of victory." },
{ "author": "Swain", "quote": "The more they try to kill me, the more they reveal I am on the right path." },
{ "author": "Swain", "quote": "The outcome was decided when they brought an army; and I brought a demon." },
{ "author": "Swain", "quote": "The right to rule, held, in my hand." },
{ "author": "Swain", "quote": "There is always a choice. The truth is no exception." },
{ "author": "Swain", "quote": "They are blind to the cold logic of this world." },
{ "author": "Swain", "quote": "They are five steps from realizing: I am ten steps ahead." },
{ "author": "Swain", "quote": "They expect me to play fairly... We aren't even playing the same game." },
{ "author": "Swain", "quote": "What is one more demon, when I already have so many?" },
{ "author": "Swain", "quote": "Would they even struggle to survive, if they knew what was to come?" },
{ "author": "Swain", "quote": "You can sit on a throne, that doesn't make you a ruler. It only means you have an arse." },
{ "author": "Tahm Kench", "quote": "All creation is born famished and starving." },
{ "author": "Tahm Kench", "quote": "Child, you're a couple cows short of a steak!" },
{ "author": "Tahm Kench", "quote": "The only real sin is to deny a craving." },
{ "author": "Tahm Kench", "quote": "We all gourmandize from time to time." },
{ "author": "Teemo", "quote": "Size doesn't mean everything." },
{ "author": "Thresh", "quote": "I am the thing under the bed." },
{ "author": "Thresh", "quote": "Me, mad? Haha... quite likely." },
{ "author": "Urgot", "quote": "Cast into a pit of despair, I climbed out on the corpses." },
{ "author": "Urgot", "quote": "I am stronger than man, stronger than machine, I am an idea." },
{ "author": "Urgot", "quote": "I am the very definition of a self-made man." },
{ "author": "Urgot", "quote": "If they do not stop me, they will die. It is just that simple." },
{ "author": "Urgot", "quote": "Pain is the act of becoming." },
{ "author": "Urgot", "quote": "We will rise from the rubble, stronger than before." },
{ "author": "Urgot", "quote": "You cannot know strength... Until you are broken." },
{ "author": "Veigar", "quote": "'It's only a short way'? Is that a short joke?!" },
{ "author": "Veigar", "quote": "I am evil! Stop laughing!" },
{ "author": "Veigar", "quote": "Know that if the tables were turned, I would show you no mercy!" },
{ "author": "Vex", "quote": "'Death is the true meaning of life.' Whoa! That's deep." },
{ "author": "Vex", "quote": "[Speaking to Lux] Oh, no. Happiness and rainbows? I'm gonna barf twice." },
{ "author": "Vex", "quote": "And then I told her, 'Get outta my room!' And she said, 'This is my house, young lady and'... Oh, hang on, Shadow. I'll finish this later." },
{ "author": "Vex", "quote": "Calm down, Shadow. I'm trying to sulk." },
{ "author": "Vex", "quote": "I am not cute. I am dark and forlorn and hopelessly morbid!" },
{ "author": "Vex", "quote": "I could start a club for people who hate people! Ehh, but no one would show up." },
{ "author": "Vex", "quote": "This is going to be... awful, in a very good way. A good, awful way. You know what I mean!" },
{ "author": "Vex", "quote": "Welcome to Sad Town. Population: Me. Everyone else get out." },
{ "author": "Viktor", "quote": "All that is logical is true, absolute, irrefutable." },
{ "author": "Viktor", "quote": "Choice is false. It is how we clothe and forgive the baser instincts that spur us to division." },
{ "author": "Viktor", "quote": "Emotion and logic cannot coexist. One must be shed to gain the other." },
{ "author": "Viktor", "quote": "Emotion... clashes with reason." },
{ "author": "Viktor", "quote": "Governed by instinct, humans are no more than flawed and flailing animals." },
{ "author": "Viktor", "quote": "Hexcorization requires no justification. What purpose is there in explaining a horseshoe to the horse?" },
{ "author": "Viktor", "quote": "Humanity... is self-corrupting." },
{ "author": "Viktor", "quote": "I am not the man I was, but who I wished to be." },
{ "author": "Viktor", "quote": "I am the only one with the means to cure suffering. But it is a lonely path." },
{ "author": "Viktor", "quote": "I chose to become this. Difficulty had no bearing nor did danger. It was... necessary." },
{ "author": "Viktor", "quote": "I did not know true... beauty, until the Arcane." },
{ "author": "Viktor", "quote": "I offer no choice, for there is none." },
{ "author": "Viktor", "quote": "I sense... trepidation. But one cannot grow if left unchallenged." },
{ "author": "Viktor", "quote": "Mankind clings to its past. Glorifies its present. And lives in dread of tomorrow." },
{ "author": "Viktor", "quote": "Passion double-crosses, subverts, divides." },
{ "author": "Viktor", "quote": "Sentiment is incompatible with control." },
{ "author": "Viktor", "quote": "So much of what we value is inconsequential." },
{ "author": "Viktor", "quote": "They think humanity can survive with emotion? Survive as what? Creatures blinded by impulse?" },
{ "author": "Viktor", "quote": "To live with flaws, is to be subject to them." },
{ "author": "Viktor", "quote": "True change must be imposed, not offered." },
{ "author": "Viktor", "quote": "What I am doing is not torment. Torment is allowing the mind to corrupt the soul." },
{ "author": "Volibear", "quote": "A thousand scars, what is one more?" },
{ "author": "Volibear", "quote": "The creations of mortals fail. The wild remains." },
{ "author": "Volibear", "quote": "The land slumbers, but it is not dead. With my roar, I wake it. With my thunder, I call it." },
{ "author": "Volibear", "quote": "They have forgotten the old ways. The old ways have not forgotten them." },
{ "author": "Volibear", "quote": "Warm-bloods rose on two legs... and forgot how to run." },
{ "author": "Xerath", "quote": "I am power incarnate! Who dares oppose me?" },
{ "author": "Xerath", "quote": "I am the will of man, unbound by flesh." },
{ "author": "Xerath", "quote": "I see the forces that hold the universe together." },
{ "author": "Xerath", "quote": "The secrets of magic are mine alone." },
{ "author": "Yone", "quote": "Long before blades and sorcery are needed, words... can save a soul." },
{ "author": "Yone", "quote": "Sleep is not for the weak, but for the blessed." },
{ "author": "Yone", "quote": "Sometimes, to save someone, you must fight them." }
]
}

View File

@@ -1,34 +1,43 @@
import { Model, Sequelize, STRING } from "sequelize";
import {Model, Sequelize, STRING} from 'sequelize';
import "node:fs/promises";
import { readFile } from "node:fs";
import 'node:fs/promises';
import {readFile} from 'node:fs';
export const sql = new Sequelize("sqlite://./blitzcrank.sqlite");
export const sequelize = new Sequelize('sqlite://./blitzcrank.sqlite');
export class GuildSetting extends Model {
// Discord ID for the Guild (server)
declare guildId: string;
// Name of the option
declare key: string;
// Value being set
declare value?: string;
}
GuildSetting.init(
{
guildId: { type: STRING },
key: { type: STRING },
value: { type: STRING },
},
{ sequelize: sql },
);
export async function initDb() {
export async function initializeDatabase() {
try {
await sql.authenticate();
console.log("Connected to database");
await sequelize.authenticate();
console.log('Connected to database');
} catch (error) {
console.error("Unable to connect to the database:", error);
console.error('Unable to connect to the database:', error);
}
}
export async function initializeModels() {
GuildSetting.init(
{
guildId: {type: STRING},
key: {type: STRING},
value: {type: STRING},
},
{sequelize},
);
}
export async function syncModels() {
await GuildSetting.sync();
}
export async function readSettingsFromFile(path: string) {
readFile(path, async (err, data) => {
try {
@@ -43,10 +52,10 @@ export async function readSettingsFromFile(path: string) {
});
}
}
GuildSetting.bulkCreate(toInsert, { updateOnDuplicate: ["value"] });
GuildSetting.bulkCreate(toInsert, {updateOnDuplicate: ['value']});
} catch (error) {
// TODO
console.log("Could not read settings:", error);
console.log('Could not read settings:', error);
}
});
}

17
environ.ts Normal file
View File

@@ -0,0 +1,17 @@
export interface EnvSettings {
BLITZCRANK_API_TOKEN: string;
BLITZCRANK_APP_ID: string;
}
export function readEnvSettings(): EnvSettings {
// TODO(DWM): I hate this.
const BLITZCRANK_API_TOKEN = process.env.BLITZCRANK_API_TOKEN;
if (BLITZCRANK_API_TOKEN === undefined || BLITZCRANK_API_TOKEN === null) {
throw TypeError();
}
const BLITZCRANK_APP_ID = process.env.BLITZCRANK_APP_ID;
if (BLITZCRANK_APP_ID === undefined || BLITZCRANK_APP_ID === null) {
throw TypeError();
}
return { BLITZCRANK_API_TOKEN, BLITZCRANK_APP_ID };
}

129
index.ts
View File

@@ -1,12 +1,3 @@
import type { Interaction } from "discord.js";
import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js";
import type {
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
} from "discord.js";
import { sql, GuildSetting, initDb } from "./database";
const BLITZCRANK_BANNER = `
****++++++++++*+++
**+*+******+********+******+*++*
@@ -62,6 +53,29 @@ const BLITZCRANK_BANNER = `
"FIRED UP AND READY TO SERVE!"
`;
import type {Interaction} from 'discord.js';
import {
Client,
Collection,
Events,
GatewayIntentBits,
MessageFlags,
REST,
Routes,
} from 'discord.js';
import {GuildSetting, initializeDatabase, initializeModels, sequelize, syncModels} from './database';
import {readEnvSettings} from './environ';
import {Command} from './plugin';
import { NagPlugin } from './plugins/nag'
import { PingPlugin } from './plugins/ping'
import { QuotePlugin } from './plugins/quote'
import { RemindPlugin } from './plugins/remind'
const envSettings = readEnvSettings();
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
@@ -70,53 +84,45 @@ const client = new Client({
GatewayIntentBits.MessageContent,
],
});
import { Routes } from "discord.js";
import { guildId, appId, token, remindersChannelId } from "./config.json";
import { REST } from "discord.js";
const rest = new REST();
rest.setToken(token);
rest.setToken(envSettings.BLITZCRANK_API_TOKEN);
interface Command {
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder;
execute: (interaction: any) => Promise<void>;
initialize: (any) => Promise<void>;
}
import { Collection } from "discord.js";
const commands = new Collection<string, Command>();
import PingCommand from "./commands/calendar/ping";
import RemindCommand from "./commands/calendar/remind";
import QuoteCommand from "./commands/quotes/quote";
const settings = {
client: client,
database: sequelize,
}
console.debug(`${remindersChannelId}`);
const plugins = [
new NagPlugin(settings),
new QuotePlugin(settings),
new RemindPlugin(settings),
new PingPlugin(settings),
]
commands.set("ping", PingCommand({ client: client, db: sql }));
commands.set(
"remind",
RemindCommand({
client: client,
db: sql,
publicChannel: remindersChannelId,
responseMode: "public",
}),
);
commands.set("quote", QuoteCommand({}));
async function syncCommands() {
try {
console.log(`Started refreshing slash commands`);
const _data = await rest.put(
Routes.applicationGuildCommands(appId, guildId),
{
body: commands.mapValues((cmd) => cmd.data.toJSON()),
},
);
console.log(`Successfully reloaded slash commands`);
} catch (error) {
console.error(error);
async function addApplicationGuildCommands() {
for (const plugin of plugins) {
for (const command of plugin.commands) {
commands.set(command.data.name, command);
}
}
for (const [_guildId, guild] of client.guilds.cache) {
try {
console.log(`Started refreshing slash commands`);
const _data = await rest.put(
Routes.applicationGuildCommands(
envSettings.BLITZCRANK_APP_ID,
guild.id,
),
{
body: commands.mapValues(cmd => cmd.data.toJSON()),
},
);
console.log(`Successfully reloaded slash commands`);
} catch (error) {
console.error(error);
}
}
}
@@ -135,7 +141,7 @@ client.on(Events.InteractionCreate, async (interaction: Interaction) => {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: "There was an error while executing this command",
content: 'There was an error while executing this command',
flags: MessageFlags.Ephemeral,
});
}
@@ -143,22 +149,17 @@ client.on(Events.InteractionCreate, async (interaction: Interaction) => {
// TODO
});
client.once(Events.ClientReady, async (readyClient) => {
await syncCommands();
initDb(); // TODO
GuildSetting.sync(); // TODO
client.once(Events.ClientReady, async readyClient => {
await initializeDatabase();
await initializeModels();
await syncModels();
await addApplicationGuildCommands();
for (const [_name, cmd] of commands) {
await cmd?.initialize({
client: client,
db: sql,
});
}
// Print banner
for (const ln of BLITZCRANK_BANNER.split("\n")) {
for (const ln of BLITZCRANK_BANNER.split('\n')) {
console.log(ln);
}
console.log(`Logged in as ${readyClient.user.tag}`);
});
client.login(token);
client.login(envSettings.BLITZCRANK_API_TOKEN);

View File

@@ -1,30 +1,34 @@
{
"name": "blitzcrank",
"version": "1.0.0",
"description": "FIRED UP AND READY TO SERVE",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Drew Malzahn",
"repository": {
"type": "git",
"url": "git+https://github.com/dwwmmn/blitzcrank.git"
},
"keywords": [
"discord",
"bot"
],
"author": "Drew Malzahn",
"license": "MIT",
"bugs": {
"url": "https://github.com/dwwmmn/blitzcrank/issues"
},
"homepage": "https://github.com/dwwmmn/blitzcrank#readme",
"main": "index.js",
"dependencies": {
"@biomejs/biome": "^2.0.6",
"chrono-node": "^2.8.3",
"discord.js": "^14.21.0",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.7",
"sqlite3": "^5.1.7"
},
"bugs": {
"url": "https://github.com/dwwmmn/blitzcrank/issues"
},
"description": "FIRED UP AND READY TO SERVE",
"homepage": "https://github.com/dwwmmn/blitzcrank#readme",
"keywords": [
"discord",
"bot"
],
"license": "MIT",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"vitest": "^3.2.4"
}
}

32
plugin.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { ChatInputCommandInteraction, Client, SlashCommandBuilder } from "discord.js";
import type { Sequelize } from "sequelize";
export interface Settings {
// Main Discord client object
client: Client;
// Database access object
database: Sequelize;
}
export interface Command {
data: SlashCommandBuilder;
execute: (iteraction: ChatInputCommandInteraction) => Promise<void>;
}
export interface Plugin {
start: () => Promise<void>;
stop: () => Promise<void>;
commands: Command[]
}
export class BasePlugin implements Plugin {
settings: Settings;
commands: Command[]
constructor(settings: Settings) {
this.settings = settings;
}
async start() {}
async stop() {}
}

44
plugins/nag/checkin.ts Normal file
View File

@@ -0,0 +1,44 @@
import {
type ChatInputCommandInteraction,
SlashCommandBuilder,
} from 'discord.js';
import type {Settings} from '../../plugin';
import {CheckIn, Nag} from './models';
export default function (_settings: Settings) {
return {
data: new SlashCommandBuilder()
.setName('checkin')
.setDescription('Check-in for your daily nag')
.addStringOption(option =>
option
.setName('text')
.setDescription('Optional description of what you have achieved'),
),
execute: async (interaction: ChatInputCommandInteraction) => {
// TODO For now there is only one nag, but in the future this could be different.
// So let's construct it as a loop for now.
const result = await Nag.findAll({
where: {
userId: interaction.user.id,
},
});
if (!result) {
await interaction.reply(
"Couldn't find any nags for you, what are you checking in for?",
);
return;
}
for (const nag of result) {
await CheckIn.create({
nagId: nag.id,
lastCheckIn: new Date(Date.now()),
});
await interaction.reply('Thanks for checking in!');
break;
}
},
};
}

99
plugins/nag/index.test.ts Normal file
View File

@@ -0,0 +1,99 @@
import {Sequelize } from 'sequelize';
import {afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
findGuiltyNags,
getCheckIn,
nextCheckInDate,
} from '.';
import { CheckIn, initializeModels, Nag } from './models'
describe('nextCheckInDate', () => {
beforeEach(() => {
vi.useFakeTimers(); // Tell vitest to use fake timers
});
afterEach(() => {
vi.useRealTimers(); // Reset date after test runs
});
it('Returns 9AM if called before 9AM that day', () => {
const now = new Date(Date.now());
const at9AM = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
9,
0,
);
vi.setSystemTime(
new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0),
);
expect(nextCheckInDate()).toEqual(at9AM);
});
it('Returns 9AM tomorrow if called after 9AM', () => {
const dayInMS = 24 * 60 * 60 * 1000;
const now = new Date(Date.now());
const tomorrow = new Date(Date.now() + dayInMS);
const tomorrow9AM = new Date(
tomorrow.getFullYear(),
tomorrow.getMonth(),
tomorrow.getDate(),
9,
0,
);
vi.setSystemTime(
new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9, 30),
);
expect(nextCheckInDate()).toEqual(tomorrow9AM);
});
});
describe('Finding nags without check-ins', async () => {
const sequelize = new Sequelize('sqlite://:memory:');
const exampleNag = {
userId: '1234',
guildId: '1234',
channelId: '1234',
messageId: '1234',
text: 'Example nag 1',
mentionHere: false,
};
await initializeModels(sequelize);
beforeEach(async () => {
vi.useFakeTimers();
await Nag.sync();
await CheckIn.sync();
});
afterEach(async () => {
await Nag.drop();
await CheckIn.drop();
vi.useRealTimers();
});
it('Finds nags without any check-ins', async () => {
const now = new Date();
vi.setSystemTime(
new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9),
);
await Nag.create(exampleNag);
const results = await findGuiltyNags();
expect(results.map(nag => nag.userId)).toEqual(['1234']);
});
it('Ignores nags with a recent check-in', async () => {
const currentCheckInTime = getCheckIn(9, 0);
vi.setSystemTime(currentCheckInTime);
const newNag = await Nag.create(exampleNag);
newNag.save();
const newCheckIn = await CheckIn.create({
nagId: newNag.id,
// 1 hour previously; i.e. we checked in before the required time
lastCheckIn: new Date(currentCheckInTime.getTime() - 60 * 60 * 1000),
});
console.log(newCheckIn.lastCheckIn);
newCheckIn.save();
const results = await findGuiltyNags();
expect(results.map(nag => nag.userId)).toEqual([]);
});
});

149
plugins/nag/index.ts Normal file
View File

@@ -0,0 +1,149 @@
import { TextChannel } from 'discord.js';
import { Op } from 'sequelize';
import { BasePlugin, type Command, type Settings } from '../../plugin'
import CheckinCommand from './checkin'
import checkin from './checkin';
import { CheckIn, initializeModels, Nag } from './models'
import NagCommand from './nag'
import UnnagCommand from './unnag'
/**
*
* @param hour Number between 0-24 representing the hour the check-in is performed.
* @param offset Number of days from today; this can be positive for future days or negative for past days.
* @returns Date object representing the check-in time
*/
export function getCheckIn(hour: number, offset: number = 0) {
const nDaysInMS = offset * 24 * 60 * 60 * 1000;
const day = new Date(Date.now() + nDaysInMS);
const checkInDate = new Date(
day.getFullYear(),
day.getMonth(),
day.getDate(),
hour,
);
return checkInDate;
}
export async function findGuiltyNags() {
const results = await Nag.findAll();
const guiltyNags: Nag[] = [];
const prevCheckIn = getCheckIn(9, -1);
const currentCheckIn = getCheckIn(9, 0);
for (const nag of results) {
const checkInResults = await CheckIn.findAll({
where: {
id: nag.id,
lastCheckIn: {
[Op.between]: [prevCheckIn, currentCheckIn],
},
},
});
if (checkInResults.length <= 0) {
guiltyNags.push(nag);
}
}
return guiltyNags;
}
/**
*
* @returns The Date representing the next check-in, which will be either
* today (if we are asking in the morning) or tomorrow (if we are asking
* after the check-in time for today.)
*/
export function nextCheckInDate() {
const todaysCheckInDate = getCheckIn(9, 0);
if (Date.now() - todaysCheckInDate.getTime() < 0) {
return getCheckIn(9, 0);
} else {
return getCheckIn(9, 1);
}
}
function nextCheckInMs() {
const delayMs = nextCheckInDate().getTime() - Date.now();
if (delayMs <= 0) {
// The value of nextCheckInDate is guaranteed to be in the future; if not, that's a bug in the program.
throw Error('Invalid value for nextCheckInDate');
}
return delayMs;
}
/**
* Handle state for the various nag-related commands, mainly related to
* tracking of timers and triggers.
*/
export class NagPlugin extends BasePlugin {
// NOTE(DWM): I really hate the word 'Manager' when applied to code, but I
// thought for quite a bit about what to name these classes and I just can't
// come up with anything solid. (Pun intended.)
interval?: NodeJS.Timeout;
constructor(settings: Settings) {
super(settings);
initializeModels(settings.database);
this.commands = [
CheckinCommand(settings) as Command, // TODO Why, TS?...
NagCommand(settings) as Command, // TODO Why, TS?...
UnnagCommand(settings),
]
}
async start() {
this.interval = setTimeout(this.loop, nextCheckInMs());
}
async triggerNag(nag: Nag) {
const client = this.settings.client;
// First, try and find the appropriate channel to send on
const guild = client.guilds.cache.get(nag.guildId);
if (!guild) {
console.error(
`No longer have access to Guild ${nag.guildId}; consider deleting this nag manually :(`,
);
return;
}
const channel = guild.channels.cache.get(nag.channelId);
if (!channel) {
console.error(
`No longer have access to Channel ${nag.channelId}; consider deleting this nag manually :(`,
);
return;
}
if (!channel?.isSendable() || !(channel instanceof TextChannel)) {
console.error("Somehow, we specified a channel which isn't sendable");
return; // TODO
}
try {
const failText =
nag.failText ??
`<@${nag.userId}> didn't complete "${nag.text}". Shame shame!`;
const mentionHere = nag.mentionHere ? '<@here> ' : '';
const msg = `${mentionHere}${failText}`;
await channel.send(msg);
} catch (error) {
console.log('Error while creating Nag:', error); // TODO
}
}
async loop() {
// According to MDN we don't need to do this, but it makes me feel happier
// knowing that we won't accidentally call clearInterval(...) on a timer
// that isn't running anymore.
this.interval = undefined;
console.debug('nag.js main loop');
const guiltyNags = await findGuiltyNags();
for (const nag of guiltyNags) {
await this.triggerNag(nag);
}
this.interval = setTimeout(this.loop, nextCheckInMs());
}
async stop() {
clearInterval(this.interval);
}
}

72
plugins/nag/models.ts Normal file
View File

@@ -0,0 +1,72 @@
import {
BOOLEAN,
DATE,
INTEGER,
Model,
type Sequelize,
STRING,
} from 'sequelize';
export class Nag extends Model {
// Primary key
declare id: number;
// User who created this nag
declare userId: string;
// Guild (server) of original nag request
declare guildId: string;
// Channel of original nag request
declare channelId: string;
// Message of original nag request
declare messageId: string;
// Description of what you're supposed to do
declare text: string;
// Custom failure text
declare failText?: string;
// Should we @here?
declare mentionHere?: boolean;
}
export class CheckIn extends Model {
// Date of the last time user ran /checkin
declare lastCheckIn: Date;
}
export async function initializeModels(sequelize: Sequelize) {
Nag.init(
{
id: {
primaryKey: true,
type: INTEGER,
autoIncrement: true,
},
userId: {
type: STRING,
allowNull: false,
},
text: {
type: STRING,
allowNull: false,
},
failText: {
type: STRING,
},
mentionHere: {
type: BOOLEAN,
},
},
{sequelize},
);
CheckIn.init(
{
lastCheckIn: {
type: DATE,
allowNull: false,
},
},
{sequelize},
);
Nag.hasMany(CheckIn);
CheckIn.belongsTo(Nag);
await Nag.sync();
await CheckIn.sync();
}

92
plugins/nag/nag.ts Normal file
View File

@@ -0,0 +1,92 @@
import {Chrono} from 'chrono-node';
import {
type ChatInputCommandInteraction,
SlashCommandBuilder,
} from 'discord.js';
import type { Settings } from '../../plugin'
import {CheckIn, Nag} from './models';
async function execute(interaction: ChatInputCommandInteraction) {
const text = interaction.options.getString('text');
if (text === null || text === undefined) {
await interaction.reply("Nag can't have a blank `text`, try again.");
return;
}
// Check if we already have an existing nag. In theory, this should be supported entirely, however
// I want to keep things simple for now.
const existingNags = await Nag.findAll({
where: {
userId: interaction.user.id,
},
// order: [["createdAt", "ASC"]],
});
console.log('Successfully looked for checkIns');
if (existingNags && existingNags.length > 0) {
// TODO: Hmm... For now, I guess we can just update the database.
for (const nag of existingNags) {
nag.text = text;
nag.failText = interaction.options.getString('failtext') ?? undefined;
await nag.save();
break;
}
await interaction.reply(
`I'll check every day at 9AM if you've completed '${text}'. If not, I'll nag you! Use /checkin to prevent a shameful callout, and /unnag to cancel.`,
);
return;
}
// Otherwise, we need to create a new nag.
const nag = await Nag.create({
userId: interaction.user.id,
guildId: interaction.guild?.id,
channelId: interaction.channel?.id,
messageId: interaction.id,
text: text,
failText: interaction.options.getString('failtext'),
mentionHere: interaction.options.getBoolean('mentionhere') ?? false,
});
await nag.save();
const chrono = new Chrono();
const checkIn = chrono.parseDate('today at 9AM');
if (!checkIn) {
await interaction.reply(
'Internal error while saving your nag. Tell Drew the bot is broken!!!',
);
return;
}
await CheckIn.create({
nag: {
id: nag.id,
},
lastCheckIn: new Date(Date.now()),
});
await interaction.reply(
`I'll check every day at 9AM if you've completed '${text}'. If not, I'll nag you! Use /checkin to prevent a shameful callout, and /unnag to cancel.`,
);
}
export default function (_settings: Settings) {
return {
data: new SlashCommandBuilder()
.setName('nag')
.setDescription('Let Blitzcrank nag you every day about something')
.addStringOption(option =>
option
.setRequired(true)
.setName('text')
.setDescription('What you have to do every day'),
)
.addStringOption(option =>
option
.setName('failtext')
.setDescription('Custom message to be broadcast on failure')
.setRequired(false),
)
.addBooleanOption(option =>
option
.setName('mentionhere')
.setDescription('Whether to DM you or @ this channel')
.setRequired(false),
),
execute,
};
}

25
plugins/nag/unnag.ts Normal file
View File

@@ -0,0 +1,25 @@
import {
type ChatInputCommandInteraction,
SlashCommandBuilder,
} from 'discord.js';
import type {Settings} from '../../plugin';
import {Nag} from './models';
export default function (_settings: Settings) {
return {
data: new SlashCommandBuilder()
.setName('unnag')
.setDescription('Remove a nag'),
execute: async (interaction: ChatInputCommandInteraction) => {
// Find all nags for this user and delete them.
// TODO In the future, we should support having multiple nags
const results = await Nag.findAll({
where: {userId: interaction.user.id},
});
for (const result of results) {
await result.destroy();
}
return;
},
};
}

22
plugins/ping.ts Normal file
View File

@@ -0,0 +1,22 @@
import {SlashCommandBuilder} from 'discord.js';
import {BasePlugin, type Settings} from '../plugin';
export class PingPlugin extends BasePlugin {
constructor(settings: Settings) {
super(settings);
this.commands = [
{
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Send a ping to the bot'),
execute: async interaction => {
await interaction.reply(
`Pong! This command was run by ${interaction.user.username}, who joined on ${interaction.member?.joinedAt}.`,
);
},
},
];
}
}

29
plugins/quote/index.ts Normal file
View File

@@ -0,0 +1,29 @@
import type {ChatInputCommandInteraction} from 'discord.js';
import {SlashCommandBuilder} from 'discord.js';
import {BasePlugin, type Settings } from '../../plugin';
import {quotes} from './quotes.json';
export class QuotePlugin extends BasePlugin {
constructor(settings: Settings) {
super(settings);
this.commands = [
{
data: new SlashCommandBuilder()
.setName('quote')
.setDescription('Print a quote from League of Legends.'),
execute: async (interaction: ChatInputCommandInteraction) => {
try {
const index = Math.floor(Math.random() * quotes.length);
await interaction.reply?.({
content: `> *${quotes[index].quote}*
> — ${quotes[index].author}`,
});
} catch (error) {
console.error(`Problem sending to channel: ${error}`);
}
},
},
];
}
}

519
plugins/quote/quotes.json Normal file
View File

@@ -0,0 +1,519 @@
{
"quotes": [
{ "author": "Blitzcrank", "quote": "A rolling golem gathers no rust." },
{ "author": "Blitzcrank", "quote": "Fired up and ready to serve." },
{ "author": "Blitzcrank", "quote": "Metal is harder than flesh." },
{
"author": "Camille",
"quote": "[Speaking to Warwick] We are all monsters. Now, you are just one on the outside."
},
{ "author": "Camille", "quote": "Efficiency is paramount to success." },
{ "author": "Camille", "quote": "Elegance never goes out of fashion." },
{
"author": "Camille",
"quote": "Extremes are easy, it's the balance that is difficult."
},
{
"author": "Camille",
"quote": "I don't play the game, I make the rules."
},
{
"author": "Camille",
"quote": "I'm what you would call a 'deniable asset'."
},
{
"author": "Camille",
"quote": "It is not the weapon that defines you, but how you wield it."
},
{
"author": "Camille",
"quote": "It's not lies that cut, but the sharpness of the truth."
},
{ "author": "Camille", "quote": "Mediocrity is the root of all evil." },
{
"author": "Camille",
"quote": "Morality is a beautiful servant and a dangerous master."
},
{
"author": "Camille",
"quote": "Precision is the difference between a butcher and a surgeon."
},
{
"author": "Camille",
"quote": "Privilege must be preserved at all costs."
},
{ "author": "Camille", "quote": "Progress is honed on necessary death." },
{
"author": "Camille",
"quote": "Progress is served by technology, not controlled by it."
},
{
"author": "Camille",
"quote": "Regret is what tempers the steel of our soul."
},
{ "author": "Camille", "quote": "Results are all that matters." },
{
"author": "Camille",
"quote": "Self-made women need to be more prevalent."
},
{
"author": "Camille",
"quote": "Sometimes scars are the most refined attire one can wear."
},
{
"author": "Camille",
"quote": "The right word cuts more deeply than a knife."
},
{
"author": "Camille",
"quote": "The task at hand is the only one that matters."
},
{
"author": "Camille",
"quote": "The world is not black or white, but a delicious shade of grey."
},
{ "author": "Camille", "quote": "Violence is a means to an end." },
{
"author": "Ekko",
"quote": "A second chance? I thought I was on my fifth!"
},
{ "author": "Ekko", "quote": "Good a time as any to act reckless." },
{
"author": "Ekko",
"quote": "It's not how much time you have, it's how you use it."
},
{ "author": "Ekko", "quote": "Never had luck. Never needed it." },
{ "author": "Ekko", "quote": "Time doesn't heal all wounds." },
{
"author": "Heimerdinger",
"quote": "42... there's just something about that number."
},
{
"author": "Heimerdinger",
"quote": "I prefer a battle of wits, but you're unarmed!"
},
{
"author": "Heimerdinger",
"quote": "Why do chemists call helium, curium, and barium 'the medical elements'? Because, if you can't 'helium' or 'curium', you 'barium'! Hm hm!"
},
{ "author": "Heimerdinger", "quote": "ヽ༼ຈل͜ຈ༽ノ raise your dongers" },
{ "author": "Jhin", "quote": "Art must exist beyond reason." },
{ "author": "Jhin", "quote": "Four!" },
{ "author": "Jhin", "quote": "I cannot be good. I must be perfection." },
{
"author": "Jhin",
"quote": "I swear each performance is the last, but I lie every time."
},
{
"author": "Jhin",
"quote": "In carnage, I bloom, like a flower in the dawn."
},
{
"author": "Jhin",
"quote": "It is by my will alone I set my mind in motion."
},
{
"author": "Jhin",
"quote": "It's fun to kill a man, to take all that he had, and could ever have."
},
{ "author": "Jhin", "quote": "You will learn what beauty truly is." },
{
"author": "Jinx",
"quote": "Fishbones, you know what we oughta do? 'Do the laundry, wash dishes and pay some bills.' Stupid dumb rocket launcher..."
},
{ "author": "Jinx", "quote": "I'm crazy! Got a doctor's note." },
{ "author": "Jinx", "quote": "I'm trying to care! But I just... can't!" },
{
"author": "Jinx",
"quote": "Rules are made to be broken... like buildings! Or people!"
},
{
"author": "Joseph Miklos",
"quote": "'It's only a short way'? Is that a short joke?!"
},
{ "author": "Joseph Miklos", "quote": "I am evil! Stop laughing!" },
{
"author": "Joseph Miklos",
"quote": "Know that if the tables were turned, I would show you no mercy!"
},
{ "author": "Joseph Miklos", "quote": "You will die by my hand!" },
{ "author": "Mordekaiser", "quote": "Ah, life is a bitter shame." },
{
"author": "Mordekaiser",
"quote": "All mortals reek with the stench of decaying flesh."
},
{
"author": "Mordekaiser",
"quote": "Fools fear death, the strong wield it."
},
{
"author": "Mordekaiser",
"quote": "I alone am the bastion between eternal existence and oblivion."
},
{
"author": "Mordekaiser",
"quote": "I carve my kingdom beyond, from the ashes of nothing, no mortals, not even gods, will stop me from claiming what is mine."
},
{
"author": "Mordekaiser",
"quote": "I have bent the realm of the dead to my will, this world shall be next."
},
{
"author": "Mordekaiser",
"quote": "I raise my iron fist to subjugate the living."
},
{
"author": "Mordekaiser",
"quote": "I will grind their petty souls into mortar."
},
{
"author": "Mordekaiser",
"quote": "I will silence the incessant thrum of mortal hearts."
},
{
"author": "Mordekaiser",
"quote": "In the world beyond, blackened ichor filled a crumbling sky, as souls withered to nothing. But I refused to fade."
},
{
"author": "Mordekaiser",
"quote": "Mortals plan in fear for tomorrow, I build for eternity."
},
{
"author": "Mordekaiser",
"quote": "Naive men pray to the gods; they will learn to pray to me."
},
{
"author": "Mordekaiser",
"quote": "Only the worthy receive the gift of Nightfall's kiss."
},
{
"author": "Mordekaiser",
"quote": "Shed the frailty of flesh, embrace the cold edge of iron."
},
{
"author": "Mordekaiser",
"quote": "The dead belong to me, the living shall be next."
},
{
"author": "Mordekaiser",
"quote": "The world has tried to forget my existence, time to remind them why they fear."
},
{ "author": "Mordekaiser", "quote": "Twice slain, thrice born." },
{
"author": "Mordekaiser",
"quote": "Weaklings cower in the light, I bring eternal darkness."
},
{
"author": "Poppy",
"quote": "I'm in, one-hundred-percent! That's everything, right?"
},
{ "author": "Poppy", "quote": "I'm no hero—just a Yordle with a hammer." },
{ "author": "Poppy", "quote": "Just had a thought—three pigtails!" },
{
"author": "Poppy",
"quote": "The hammer does most of the work, I just swing it."
},
{ "author": "Rammus", "quote": "OK." },
{ "author": "Rammus", "quote": "🆗" },
{ "author": "Senna", "quote": "I forgive. No one else has to." },
{
"author": "Senna",
"quote": "I remember my nightmares. Wish I could remember to dream."
},
{ "author": "Sion", "quote": "A black eye for the earth!" },
{ "author": "Sion", "quote": "Death had its chance." },
{ "author": "Sion", "quote": "Noxus suffers no cowards." },
{ "author": "Sion", "quote": "The quiet... eats at me." },
{ "author": "Swain", "quote": "A calculated risk is no risk at all." },
{
"author": "Swain",
"quote": "A new vantage, is all the advantage I need."
},
{ "author": "Swain", "quote": "And to think, they called me a 'cripple'." },
{ "author": "Swain", "quote": "Destiny marches—like any man." },
{ "author": "Swain", "quote": "Diplomacy is a subtle art." },
{
"author": "Swain",
"quote": "Hmph... I do so enjoy explaining things to idiots."
},
{
"author": "Swain",
"quote": "I cannot lead if I allow fools to stumble about before me."
},
{
"author": "Swain",
"quote": "I could kill them all. But it would be far crueler to show them that I am right."
},
{
"author": "Swain",
"quote": "I have killed more men with words than by my own hand. Not for lack of trying."
},
{
"author": "Swain",
"quote": "I suppose I should be grateful they have the decency to fear me."
},
{
"author": "Swain",
"quote": "I've heard what they call me. What a waste of their final words."
},
{
"author": "Swain",
"quote": "If they already call me a villain, what will they call me when I succeed?"
},
{ "author": "Swain", "quote": "Is it not enough for Noxus to be strong?" },
{
"author": "Swain",
"quote": "It is not the visions that haunt me—but what I do not see."
},
{
"author": "Swain",
"quote": "Never make a bargain with a demon... that you intend to keep."
},
{
"author": "Swain",
"quote": "One can read the future in battle lines, assuming one can read."
},
{
"author": "Swain",
"quote": "People often ask for a hero, when a villain is what they truly need."
},
{
"author": "Swain",
"quote": "Pity stays the hand of the merciful, but not mine."
},
{
"author": "Swain",
"quote": "Tell me again all the crimes I've committed, and I'll tell you the price of victory."
},
{
"author": "Swain",
"quote": "The more they try to kill me, the more they reveal I am on the right path."
},
{
"author": "Swain",
"quote": "The outcome was decided when they brought an army; and I brought a demon."
},
{ "author": "Swain", "quote": "The right to rule, held, in my hand." },
{
"author": "Swain",
"quote": "There is always a choice. The truth is no exception."
},
{
"author": "Swain",
"quote": "They are blind to the cold logic of this world."
},
{
"author": "Swain",
"quote": "They are five steps from realizing: I am ten steps ahead."
},
{
"author": "Swain",
"quote": "They expect me to play fairly... We aren't even playing the same game."
},
{
"author": "Swain",
"quote": "What is one more demon, when I already have so many?"
},
{
"author": "Swain",
"quote": "Would they even struggle to survive, if they knew what was to come?"
},
{
"author": "Swain",
"quote": "You can sit on a throne, that doesn't make you a ruler. It only means you have an arse."
},
{
"author": "Tahm Kench",
"quote": "All creation is born famished and starving."
},
{
"author": "Tahm Kench",
"quote": "Child, you're a couple cows short of a steak!"
},
{
"author": "Tahm Kench",
"quote": "The only real sin is to deny a craving."
},
{
"author": "Tahm Kench",
"quote": "We all gourmandize from time to time."
},
{ "author": "Teemo", "quote": "Size doesn't mean everything." },
{ "author": "Thresh", "quote": "I am the thing under the bed." },
{ "author": "Thresh", "quote": "Me, mad? Haha... quite likely." },
{
"author": "Urgot",
"quote": "Cast into a pit of despair, I climbed out on the corpses."
},
{
"author": "Urgot",
"quote": "I am stronger than man, stronger than machine, I am an idea."
},
{
"author": "Urgot",
"quote": "I am the very definition of a self-made man."
},
{
"author": "Urgot",
"quote": "If they do not stop me, they will die. It is just that simple."
},
{ "author": "Urgot", "quote": "Pain is the act of becoming." },
{
"author": "Urgot",
"quote": "We will rise from the rubble, stronger than before."
},
{
"author": "Urgot",
"quote": "You cannot know strength... Until you are broken."
},
{
"author": "Veigar",
"quote": "'It's only a short way'? Is that a short joke?!"
},
{ "author": "Veigar", "quote": "I am evil! Stop laughing!" },
{
"author": "Veigar",
"quote": "Know that if the tables were turned, I would show you no mercy!"
},
{
"author": "Vex",
"quote": "'Death is the true meaning of life.' Whoa! That's deep."
},
{
"author": "Vex",
"quote": "[Speaking to Lux] Oh, no. Happiness and rainbows? I'm gonna barf twice."
},
{
"author": "Vex",
"quote": "And then I told her, 'Get outta my room!' And she said, 'This is my house, young lady and'... Oh, hang on, Shadow. I'll finish this later."
},
{ "author": "Vex", "quote": "Calm down, Shadow. I'm trying to sulk." },
{
"author": "Vex",
"quote": "I am not cute. I am dark and forlorn and hopelessly morbid!"
},
{
"author": "Vex",
"quote": "I could start a club for people who hate people! Ehh, but no one would show up."
},
{
"author": "Vex",
"quote": "This is going to be... awful, in a very good way. A good, awful way. You know what I mean!"
},
{
"author": "Vex",
"quote": "Welcome to Sad Town. Population: Me. Everyone else get out."
},
{
"author": "Viktor",
"quote": "All that is logical is true, absolute, irrefutable."
},
{
"author": "Viktor",
"quote": "Choice is false. It is how we clothe and forgive the baser instincts that spur us to division."
},
{
"author": "Viktor",
"quote": "Emotion and logic cannot coexist. One must be shed to gain the other."
},
{ "author": "Viktor", "quote": "Emotion... clashes with reason." },
{
"author": "Viktor",
"quote": "Governed by instinct, humans are no more than flawed and flailing animals."
},
{
"author": "Viktor",
"quote": "Hexcorization requires no justification. What purpose is there in explaining a horseshoe to the horse?"
},
{ "author": "Viktor", "quote": "Humanity... is self-corrupting." },
{
"author": "Viktor",
"quote": "I am not the man I was, but who I wished to be."
},
{
"author": "Viktor",
"quote": "I am the only one with the means to cure suffering. But it is a lonely path."
},
{
"author": "Viktor",
"quote": "I chose to become this. Difficulty had no bearing nor did danger. It was... necessary."
},
{
"author": "Viktor",
"quote": "I did not know true... beauty, until the Arcane."
},
{ "author": "Viktor", "quote": "I offer no choice, for there is none." },
{
"author": "Viktor",
"quote": "I sense... trepidation. But one cannot grow if left unchallenged."
},
{
"author": "Viktor",
"quote": "Mankind clings to its past. Glorifies its present. And lives in dread of tomorrow."
},
{
"author": "Viktor",
"quote": "Passion double-crosses, subverts, divides."
},
{ "author": "Viktor", "quote": "Sentiment is incompatible with control." },
{
"author": "Viktor",
"quote": "So much of what we value is inconsequential."
},
{
"author": "Viktor",
"quote": "They think humanity can survive with emotion? Survive as what? Creatures blinded by impulse?"
},
{
"author": "Viktor",
"quote": "To live with flaws, is to be subject to them."
},
{
"author": "Viktor",
"quote": "True change must be imposed, not offered."
},
{
"author": "Viktor",
"quote": "What I am doing is not torment. Torment is allowing the mind to corrupt the soul."
},
{ "author": "Volibear", "quote": "A thousand scars, what is one more?" },
{
"author": "Volibear",
"quote": "The creations of mortals fail. The wild remains."
},
{
"author": "Volibear",
"quote": "The land slumbers, but it is not dead. With my roar, I wake it. With my thunder, I call it."
},
{
"author": "Volibear",
"quote": "They have forgotten the old ways. The old ways have not forgotten them."
},
{
"author": "Volibear",
"quote": "Warm-bloods rose on two legs... and forgot how to run."
},
{
"author": "Xerath",
"quote": "I am power incarnate! Who dares oppose me?"
},
{ "author": "Xerath", "quote": "I am the will of man, unbound by flesh." },
{
"author": "Xerath",
"quote": "I see the forces that hold the universe together."
},
{ "author": "Xerath", "quote": "The secrets of magic are mine alone." },
{
"author": "Yone",
"quote": "Long before blades and sorcery are needed, words... can save a soul."
},
{
"author": "Yone",
"quote": "Sleep is not for the weak, but for the blessed."
},
{
"author": "Yone",
"quote": "Sometimes, to save someone, you must fight them."
}
]
}

136
plugins/remind/index.ts Normal file
View File

@@ -0,0 +1,136 @@
import * as chrono from 'chrono-node';
import {
type ChatInputCommandInteraction,
SlashCommandBuilder,
TextChannel,
} from 'discord.js';
import * as sequelize from 'sequelize';
import {BasePlugin, type Command, type Settings} from '../../plugin';
import {initializeModels, Reminder, syncModels} from './models';
async function triggerReminder(channel: TextChannel, reminder: Reminder) {
try {
await channel.send({
content: `Reminder for <@${reminder.userId}>: ${reminder.text}`,
});
reminder.isValid = false;
} catch (error) {
console.log(`Error trigggering Reminder ${reminder.id}: ${error}`);
} finally {
await reminder.save();
}
}
export class RemindPlugin extends BasePlugin {
interval: NodeJS.Timeout;
constructor(settings: Settings) {
super(settings);
initializeModels(settings.database);
this.commands = [
{
data: new SlashCommandBuilder()
.setName('remind')
.setDescription('Remind me to do something')
.addStringOption(option =>
option
.setName('when')
.setDescription('Short description of when you want the reminder')
.setRequired(true),
)
.addStringOption(option =>
option
.setName('what')
.setDescription(
'Short description of what you want to be reminded of',
)
.setRequired(true),
),
execute: async (interaction: ChatInputCommandInteraction) => {
const whenString = interaction.options.getString('when') ?? 'now';
const when = chrono.parseDate(whenString);
if (!when) {
await interaction.reply(
`Sorry, I don't understand '${when}' as a date`,
);
return;
}
const reminder = await Reminder.create({
userId: interaction.user.id,
text: interaction.options.getString('what'),
trigger: when, // TODO
requestMessage: interaction.id,
requestChannel: interaction.channelId,
isValid: true,
});
reminder.save();
await interaction.reply(`Ok, I'll remind you at ${when}`);
console.info(
`Created Reminder ${reminder.id} to be triggered at ${reminder.trigger}`,
);
},
} as Command, // TODO
];
}
getChannel(reminder: Reminder) {
// TODO I don't really know what else needs to go into this function
// I had some permissions stuff but it seemed very out-of-place
// I don't know why isSendable() doesn't cover permissions? Or maybe
// it does?
// Either way this function kind of smells
const client = this.settings.client;
const channel = client.channels.cache.get(reminder.requestChannel);
if (!(channel instanceof TextChannel)) {
throw TypeError("Can't send to a non-ext channel");
}
if (!channel) {
throw Error('Cannot find valid channel to send reminder on');
}
if (!channel.isSendable()) {
throw Error("Can't send to that channel");
}
return channel;
}
async loop() {
console.debug('remind.js main loop');
const results = await Reminder.findAll({
// NOTE: 'trigger' is the time when the reminder should go off. If trigger -
// now is positive, trigger is in the future. If trigger - now <=
// 1.0, then we have less than one minute before the timer should go
// off.
where: sequelize.literal(
"isValid AND (julianday(trigger) - julianday('now')) <= 1.0",
),
});
for (const reminder of results) {
try {
const now = new Date(Date.now());
const delay = reminder.trigger.getTime() - now.getTime();
console.log(
`Callback for Reminder ${reminder.get('id')} triggering in ${delay}ms`,
);
const channel = this.getChannel(reminder);
setTimeout(async () => await triggerReminder(channel, reminder), delay);
} catch (error) {
console.error(
`Error while processing Reminder ${reminder.id}: ${error}`,
);
console.trace();
}
}
}
async start() {
await syncModels();
this.interval = setInterval(this.loop.bind(this), 1000 * 60);
}
async stop() {
clearInterval(this.interval);
}
}

41
plugins/remind/models.ts Normal file
View File

@@ -0,0 +1,41 @@
import { BOOLEAN, DATE, INTEGER, Model, Sequelize, TEXT } from "sequelize";
export class Reminder extends Model {
declare id: number;
declare userId: string;
declare text: string | null;
declare trigger: Date;
declare isValid: boolean;
declare requestChannel: string;
declare requestMessage: string;
}
export function initializeModels(sequelize: Sequelize) {
Reminder.init(
{
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
},
userId: {
type: TEXT,
allowNull: false,
},
text: TEXT,
trigger: {
type: DATE,
allowNull: false,
},
isValid: BOOLEAN,
requestChannel: TEXT,
requestMessage: TEXT,
},
{sequelize},
);
// await Reminder.sync(); // TODO
}
export async function syncModels() {
await Reminder.sync();
}