diff --git a/.env.example b/.env.example index 8f57111..a0488d0 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,5 @@ DISCORD_CLIENT_ID= DISCORD_GUILD_ID= DATABASE_URL=postgresql://booster_role:booster_role@localhost:5432/booster_role BOOSTER_ROLE_ANCHOR_ROLE_ID= +BOOSTER_ELIGIBILITY_ROLE_ID=1206431347925852162 +LOG_LEVEL=info diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 87e6e1b..840d5a5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -55,11 +55,13 @@ jobs: DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} DATABASE_URL: ${{ secrets.DATABASE_URL }} BOOSTER_ROLE_ANCHOR_ROLE_ID: ${{ secrets.BOOSTER_ROLE_ANCHOR_ROLE_ID }} + BOOSTER_ELIGIBILITY_ROLE_ID: ${{ secrets.BOOSTER_ELIGIBILITY_ROLE_ID }} + LOG_LEVEL: ${{ secrets.LOG_LEVEL }} with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} - envs: IMAGE_NAME,GHCR_USERNAME,GHCR_TOKEN,DISCORD_TOKEN,DISCORD_CLIENT_ID,DISCORD_GUILD_ID,DATABASE_URL,BOOSTER_ROLE_ANCHOR_ROLE_ID + envs: IMAGE_NAME,GHCR_USERNAME,GHCR_TOKEN,DISCORD_TOKEN,DISCORD_CLIENT_ID,DISCORD_GUILD_ID,DATABASE_URL,BOOSTER_ROLE_ANCHOR_ROLE_ID,BOOSTER_ELIGIBILITY_ROLE_ID,LOG_LEVEL script: | set -eu mkdir -p /opt/booster-role @@ -71,6 +73,8 @@ jobs: DISCORD_GUILD_ID=${DISCORD_GUILD_ID} DATABASE_URL=${DATABASE_URL:?DATABASE_URL is required} BOOSTER_ROLE_ANCHOR_ROLE_ID=${BOOSTER_ROLE_ANCHOR_ROLE_ID} + BOOSTER_ELIGIBILITY_ROLE_ID=${BOOSTER_ELIGIBILITY_ROLE_ID:-1206431347925852162} + LOG_LEVEL=${LOG_LEVEL:-info} EOF cat > docker-compose.yml <=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="], @@ -120,6 +131,14 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], + + "color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], + + "color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], + + "color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], @@ -134,6 +153,8 @@ "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], @@ -142,8 +163,12 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -158,10 +183,16 @@ "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], @@ -170,12 +201,16 @@ "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], "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=="], @@ -190,6 +225,8 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -200,6 +237,8 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], @@ -208,6 +247,10 @@ "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=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -224,6 +267,10 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], diff --git a/package.json b/package.json index 1e5dead..58933f7 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "@discordjs/builders": "latest", "discord.js": "latest", "drizzle-orm": "latest", - "postgres": "latest" + "postgres": "latest", + "winston": "^3.19.0" }, "devDependencies": { "@types/bun": "^1.3.14", diff --git a/src/config/index.ts b/src/config/index.ts index 1fedbb0..ede5a1b 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -4,6 +4,7 @@ export type AppConfig = { discordGuildId: string; databaseUrl: string; boosterRoleAnchorRoleId: string | null; + boosterEligibilityRoleId: string; }; export function loadConfig(env: Record = process.env): AppConfig { @@ -12,7 +13,8 @@ export function loadConfig(env: Record = process.env discordClientId: requireEnv(env, "DISCORD_CLIENT_ID"), discordGuildId: requireEnv(env, "DISCORD_GUILD_ID"), databaseUrl: env.DATABASE_URL ?? "postgresql://booster_role:booster_role@localhost:5432/booster_role", - boosterRoleAnchorRoleId: env.BOOSTER_ROLE_ANCHOR_ROLE_ID ?? null + boosterRoleAnchorRoleId: env.BOOSTER_ROLE_ANCHOR_ROLE_ID ?? null, + boosterEligibilityRoleId: env.BOOSTER_ELIGIBILITY_ROLE_ID ?? "1206431347925852162" }; } diff --git a/src/discord/bot.ts b/src/discord/bot.ts index 0ff4fec..e052748 100644 --- a/src/discord/bot.ts +++ b/src/discord/bot.ts @@ -21,7 +21,7 @@ export function attachBotHandlers(client: Client, config: AppConfig): void { ); await handleInteraction(interaction, service, { - isBoosting: async () => Boolean(interaction.member && "premiumSince" in interaction.member && interaction.member.premiumSince) + isBoosting: async () => memberHasRole(interaction.member, config.boosterEligibilityRoleId) }); }); @@ -32,13 +32,31 @@ export function attachBotHandlers(client: Client, config: AppConfig): void { { anchorPosition: resolveAnchorPosition(newMember.guild, config.boosterRoleAnchorRoleId) } ); - await handleGuildMemberUpdate(oldMember, newMember, service); + await handleGuildMemberUpdate(oldMember, newMember, service, config.boosterEligibilityRoleId); }); } +function memberHasRole(member: unknown, roleId: string): boolean { + return Boolean( + member && + typeof member === "object" && + "roles" in member && + member.roles && + typeof member.roles === "object" && + "cache" in member.roles && + member.roles.cache && + typeof member.roles.cache === "object" && + "has" in member.roles.cache && + typeof member.roles.cache.has === "function" && + member.roles.cache.has(roleId) + ); +} + function resolveAnchorPosition(guild: { members: { me: { roles: { highest: { position: number } } } | null }; roles: { cache: { get(id: string): { position: number } | undefined } } }, anchorRoleId: string | null): number { - if (!anchorRoleId) return botRolePosition(guild); - return guild.roles.cache.get(anchorRoleId)?.position ?? botRolePosition(guild); + const botPosition = botRolePosition(guild); + if (!anchorRoleId) return botPosition; + const anchorPosition = guild.roles.cache.get(anchorRoleId)?.position ?? botPosition; + return Math.min(anchorPosition, botPosition); } function botRolePosition(guild: { members: { me: { roles: { highest: { position: number } } } | null } }): number { diff --git a/src/discord/events/guildMemberUpdate.test.ts b/src/discord/events/guildMemberUpdate.test.ts new file mode 100644 index 0000000..c60a7d8 --- /dev/null +++ b/src/discord/events/guildMemberUpdate.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "bun:test"; +import { handleGuildMemberUpdate } from "./guildMemberUpdate"; + +class FakeService { + removed: Array<{ guildId: string; userId: string }> = []; + + async removeRoleForLostBoost(input: { guildId: string; userId: string }): Promise { + this.removed.push(input); + } +} + +function member(hasRole: boolean) { + return { + id: "user", + guild: { id: "guild" }, + roles: { cache: { has: (roleId: string) => roleId === "booster-role" && hasRole } } + }; +} + +describe("handleGuildMemberUpdate", () => { + test("removes custom role when booster eligibility role is removed", async () => { + const service = new FakeService(); + + await handleGuildMemberUpdate(member(true), member(false), service, "booster-role"); + + expect(service.removed).toEqual([{ guildId: "guild", userId: "user" }]); + }); + + test("does nothing while booster eligibility role remains", async () => { + const service = new FakeService(); + + await handleGuildMemberUpdate(member(true), member(true), service, "booster-role"); + + expect(service.removed).toEqual([]); + }); +}); diff --git a/src/discord/events/guildMemberUpdate.ts b/src/discord/events/guildMemberUpdate.ts index 2eaa39a..05d7b60 100644 --- a/src/discord/events/guildMemberUpdate.ts +++ b/src/discord/events/guildMemberUpdate.ts @@ -1,16 +1,23 @@ -import type { BoosterRoleService } from "../../services/boosterRoleService"; - type GuildMemberLike = { id: string; - premiumSince: Date | null; guild: { id: string }; + roles: { cache: { has(roleId: string): boolean } }; }; -export async function handleGuildMemberUpdate(oldMember: GuildMemberLike, newMember: GuildMemberLike, service: BoosterRoleService): Promise { - const hadBoost = Boolean(oldMember.premiumSince); - const hasBoost = Boolean(newMember.premiumSince); +type BoosterRoleRemovalService = { + removeRoleForLostBoost(input: { guildId: string; userId: string }): Promise; +}; - if (hadBoost && !hasBoost) { +export async function handleGuildMemberUpdate( + oldMember: GuildMemberLike, + newMember: GuildMemberLike, + service: BoosterRoleRemovalService, + boosterEligibilityRoleId: string +): Promise { + const hadBoosterRole = oldMember.roles.cache.has(boosterEligibilityRoleId); + const hasBoosterRole = newMember.roles.cache.has(boosterEligibilityRoleId); + + if (hadBoosterRole && !hasBoosterRole) { await service.removeRoleForLostBoost({ guildId: newMember.guild.id, userId: newMember.id }); } } diff --git a/src/discord/interactionHandler.ts b/src/discord/interactionHandler.ts index ab2fc00..6cc4510 100644 --- a/src/discord/interactionHandler.ts +++ b/src/discord/interactionHandler.ts @@ -1,4 +1,5 @@ import { MessageFlags, PermissionFlagsBits } from "discord.js"; +import { logger } from "../logger"; import type { BoosterRoleRecord, RoleIcon } from "../services/boosterRoleService"; export type ChatInputInteractionLike = { @@ -41,6 +42,7 @@ export async function handleInteraction( const subcommand = interaction.options.getSubcommand(); if (subcommand === "claim") { + logger.info("Handling booster-role command", { guildId, userId, subcommand }); const role = await service.claimRole({ guildId, userId, @@ -54,24 +56,28 @@ export async function handleInteraction( } if (subcommand === "rename") { + logger.info("Handling booster-role command", { guildId, userId, subcommand }); await service.renameRole({ guildId, userId, name: requireString(interaction, "name") }); await interaction.reply({ content: "Booster role renamed.", flags: MessageFlags.Ephemeral }); return; } if (subcommand === "recolor") { + logger.info("Handling booster-role command", { guildId, userId, subcommand }); await service.recolorRole({ guildId, userId, color: requireString(interaction, "color") }); await interaction.reply({ content: "Booster role color updated.", flags: MessageFlags.Ephemeral }); return; } if (subcommand === "icon") { + logger.info("Handling booster-role command", { guildId, userId, subcommand }); await service.setRoleIcon({ guildId, userId, icon: requireIcon(interaction, "image") }); await interaction.reply({ content: "Booster role icon updated.", flags: MessageFlags.Ephemeral }); return; } if (subcommand === "delete") { + logger.info("Handling booster-role command", { guildId, userId, subcommand }); await service.deleteRole({ guildId, userId }); await interaction.reply({ content: "Booster role deleted.", flags: MessageFlags.Ephemeral }); return; @@ -79,13 +85,16 @@ export async function handleInteraction( if (subcommand === "admin-delete") { requireAdministrator(interaction); - await service.deleteRole({ guildId, userId: requireUser(interaction, "user").id }); + const targetUserId = requireUser(interaction, "user").id; + logger.info("Handling booster-role admin command", { guildId, userId, subcommand, targetUserId }); + await service.deleteRole({ guildId, userId: targetUserId }); await interaction.reply({ content: "Booster role deleted by admin.", flags: MessageFlags.Ephemeral }); return; } throw new Error("Unknown booster-role subcommand"); } catch (error) { + logger.warn("Booster-role command failed", { error }); await interaction.reply({ content: error instanceof Error ? error.message : "Command failed", flags: MessageFlags.Ephemeral }); } } diff --git a/src/index.ts b/src/index.ts index d0cb6ae..1c3b0b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,18 +2,22 @@ import { loadConfig } from "./config"; import { attachBotHandlers } from "./discord/bot"; import { createDiscordClient } from "./discord/client"; import { registerGuildCommandsWithToken } from "./discord/registerCommands"; +import { logger } from "./logger"; const config = loadConfig(); +logger.info("Registering guild commands", { guildId: config.discordGuildId }); await registerGuildCommandsWithToken(config.discordToken, { clientId: config.discordClientId, guildId: config.discordGuildId }); +logger.info("Guild commands registered", { guildId: config.discordGuildId }); const client = createDiscordClient(); attachBotHandlers(client, config); client.once("clientReady", () => { - console.log(`Logged in as ${client.user?.tag ?? "unknown bot"}`); + logger.info("Discord client ready", { bot: client.user?.tag ?? "unknown bot" }); }); +logger.info("Logging in Discord client"); await client.login(config.discordToken); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..203afe3 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,11 @@ +import winston from "winston"; + +export const logger = winston.createLogger({ + level: process.env.LOG_LEVEL ?? "info", + format: winston.format.combine( + winston.format.timestamp(), + winston.format.errors({ stack: true }), + winston.format.json() + ), + transports: [new winston.transports.Console()] +}); diff --git a/src/services/discordRoleRepository.ts b/src/services/discordRoleRepository.ts index e9cd936..5aa074d 100644 --- a/src/services/discordRoleRepository.ts +++ b/src/services/discordRoleRepository.ts @@ -40,10 +40,7 @@ export class DiscordRoleRepository implements RoleRepository { } private async resolvePosition(fallbackPosition: number): Promise { - if (!this.anchorRoleId) return fallbackPosition; - - const anchor = await this.fetchRole(this.anchorRoleId); - return Math.max(anchor.position, 1); + return Math.max(fallbackPosition, 1); } private async fetchRole(roleId: string): Promise {