diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0b76e69..b90bc2b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -26,7 +26,9 @@ jobs: run: cp $HOME/files.json ./scripts/validate_tags/ - name: Validate tags - run: bun run validate:tags + run: | + bun i + bun run validate:tags env: github-token: ${{ secrets.GITHUB_TOKEN }} commit-sha: ${{ github.event.pull_request.head.sha }} diff --git a/.gitignore b/.gitignore index 54d3912..9f30e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -80,10 +80,7 @@ web_modules/ # dotenv environment variable files .env -.env.development.local -.env.test.local -.env.production.local -.env.local +.env.* # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/README.md b/README.md index cd60b85..1c878d3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Bun discord bot -Official serverless discord bot for [bun](https://bun.sh/) [discord server](https://bun.sh/discord) +Official discord bot for [bun](https://bun.sh/) [discord server](https://bun.sh/discord) ## Contributing tags To create a new tag, you need to fork this repository and add new file to `data/tags/name.md` file diff --git a/bun.lockb b/bun.lockb index 6aee8d4..8733e23 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/data/tags/windows.md b/data/tags/windows.md index 6648563..af54ea2 100644 --- a/data/tags/windows.md +++ b/data/tags/windows.md @@ -5,5 +5,12 @@ keywords: - "windows support" --- -Bun does not currently have support for Windows, so you must use WSL (Windows Subsystem for Linux). -To install WSL, check [microsoft documentation]() +Bun provides a *limited*, experimental native builds for [Windows](). +To install, paste this into your terminal: +```ps +# WARNING: No stability is guaranteed on the experimental Windows builds +powershell -c "iwr bun.sh/install.ps1|iex" +``` + +However, it's still recommended to use [WSL]() for the best experience. +For more information, you can look into <#1149339379446325248> \ No newline at end of file diff --git a/globals.d.ts b/globals.d.ts new file mode 100644 index 0000000..857af7f --- /dev/null +++ b/globals.d.ts @@ -0,0 +1,7 @@ +declare module "bun" { + interface Env { + DISCORD_BOT_TOKEN: string; + BUN_ONLY_CHANNEL_ID: string; + MESSAGE_PREFIX: string; + } +} diff --git a/package.json b/package.json index 852d268..d12d0e7 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,22 @@ { "version": "0.0.0", + "private": true, "name": "bun-discord-bot", + "type": "module", "scripts": { - "start": "bun src/index.ts", - "validate:tags": "cd scripts/validate_tags && bun install && bun start" + "start": "NODE_ENV=production bun src/index.ts", + "validate:tags": "bun scripts/validate_tags/index.ts" }, "devDependencies": { - "bun-types": "^0.7.3" + "bun-types": "^1.0.14" }, "dependencies": { + "@lilybird/handlers": "^0.2.1", + "@lilybird/jsx": "^0.2.0", "@paperdave/logger": "^3.0.1", "algoliasearch": "^4.19.1", - "discord.js": "^14.13.0", - "glob": "^10.3.3", "gray-matter": "^4.0.3", + "lilybird": "^0.4.0", "zlib-sync": "^0.1.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 9e5b53e..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,672 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@paperdave/logger': - specifier: ^3.0.1 - version: 3.0.1 - algoliasearch: - specifier: ^4.19.1 - version: 4.19.1 - discord.js: - specifier: ^14.13.0 - version: 14.13.0 - glob: - specifier: ^10.3.3 - version: 10.3.3 - gray-matter: - specifier: ^4.0.3 - version: 4.0.3 - zlib-sync: - specifier: ^0.1.8 - version: 0.1.8 - -devDependencies: - bun-types: - specifier: ^0.7.3 - version: 0.7.3 - -packages: - - /@algolia/cache-browser-local-storage@4.19.1: - resolution: {integrity: sha512-FYAZWcGsFTTaSAwj9Std8UML3Bu8dyWDncM7Ls8g+58UOe4XYdlgzXWbrIgjaguP63pCCbMoExKr61B+ztK3tw==} - dependencies: - '@algolia/cache-common': 4.19.1 - dev: false - - /@algolia/cache-common@4.19.1: - resolution: {integrity: sha512-XGghi3l0qA38HiqdoUY+wvGyBsGvKZ6U3vTiMBT4hArhP3fOGLXpIINgMiiGjTe4FVlTa5a/7Zf2bwlIHfRqqg==} - dev: false - - /@algolia/cache-in-memory@4.19.1: - resolution: {integrity: sha512-+PDWL+XALGvIginigzu8oU6eWw+o76Z8zHbBovWYcrtWOEtinbl7a7UTt3x3lthv+wNuFr/YD1Gf+B+A9V8n5w==} - dependencies: - '@algolia/cache-common': 4.19.1 - dev: false - - /@algolia/client-account@4.19.1: - resolution: {integrity: sha512-Oy0ritA2k7AMxQ2JwNpfaEcgXEDgeyKu0V7E7xt/ZJRdXfEpZcwp9TOg4TJHC7Ia62gIeT2Y/ynzsxccPw92GA==} - dependencies: - '@algolia/client-common': 4.19.1 - '@algolia/client-search': 4.19.1 - '@algolia/transporter': 4.19.1 - dev: false - - /@algolia/client-analytics@4.19.1: - resolution: {integrity: sha512-5QCq2zmgdZLIQhHqwl55ZvKVpLM3DNWjFI4T+bHr3rGu23ew2bLO4YtyxaZeChmDb85jUdPDouDlCumGfk6wOg==} - dependencies: - '@algolia/client-common': 4.19.1 - '@algolia/client-search': 4.19.1 - '@algolia/requester-common': 4.19.1 - '@algolia/transporter': 4.19.1 - dev: false - - /@algolia/client-common@4.19.1: - resolution: {integrity: sha512-3kAIVqTcPrjfS389KQvKzliC559x+BDRxtWamVJt8IVp7LGnjq+aVAXg4Xogkur1MUrScTZ59/AaUd5EdpyXgA==} - dependencies: - '@algolia/requester-common': 4.19.1 - '@algolia/transporter': 4.19.1 - dev: false - - /@algolia/client-personalization@4.19.1: - resolution: {integrity: sha512-8CWz4/H5FA+krm9HMw2HUQenizC/DxUtsI5oYC0Jxxyce1vsr8cb1aEiSJArQT6IzMynrERif1RVWLac1m36xw==} - dependencies: - '@algolia/client-common': 4.19.1 - '@algolia/requester-common': 4.19.1 - '@algolia/transporter': 4.19.1 - dev: false - - /@algolia/client-search@4.19.1: - resolution: {integrity: sha512-mBecfMFS4N+yK/p0ZbK53vrZbL6OtWMk8YmnOv1i0LXx4pelY8TFhqKoTit3NPVPwoSNN0vdSN9dTu1xr1XOVw==} - dependencies: - '@algolia/client-common': 4.19.1 - '@algolia/requester-common': 4.19.1 - '@algolia/transporter': 4.19.1 - dev: false - - /@algolia/logger-common@4.19.1: - resolution: {integrity: sha512-i6pLPZW/+/YXKis8gpmSiNk1lOmYCmRI6+x6d2Qk1OdfvX051nRVdalRbEcVTpSQX6FQAoyeaui0cUfLYW5Elw==} - dev: false - - /@algolia/logger-console@4.19.1: - resolution: {integrity: sha512-jj72k9GKb9W0c7TyC3cuZtTr0CngLBLmc8trzZlXdfvQiigpUdvTi1KoWIb2ZMcRBG7Tl8hSb81zEY3zI2RlXg==} - dependencies: - '@algolia/logger-common': 4.19.1 - dev: false - - /@algolia/requester-browser-xhr@4.19.1: - resolution: {integrity: sha512-09K/+t7lptsweRTueHnSnmPqIxbHMowejAkn9XIcJMLdseS3zl8ObnS5GWea86mu3vy4+8H+ZBKkUN82Zsq/zg==} - dependencies: - '@algolia/requester-common': 4.19.1 - dev: false - - /@algolia/requester-common@4.19.1: - resolution: {integrity: sha512-BisRkcWVxrDzF1YPhAckmi2CFYK+jdMT60q10d7z3PX+w6fPPukxHRnZwooiTUrzFe50UBmLItGizWHP5bDzVQ==} - dev: false - - /@algolia/requester-node-http@4.19.1: - resolution: {integrity: sha512-6DK52DHviBHTG2BK/Vv2GIlEw7i+vxm7ypZW0Z7vybGCNDeWzADx+/TmxjkES2h15+FZOqVf/Ja677gePsVItA==} - dependencies: - '@algolia/requester-common': 4.19.1 - dev: false - - /@algolia/transporter@4.19.1: - resolution: {integrity: sha512-nkpvPWbpuzxo1flEYqNIbGz7xhfhGOKGAZS7tzC+TELgEmi7z99qRyTfNSUlW7LZmB3ACdnqAo+9A9KFBENviQ==} - dependencies: - '@algolia/cache-common': 4.19.1 - '@algolia/logger-common': 4.19.1 - '@algolia/requester-common': 4.19.1 - dev: false - - /@discordjs/builders@1.6.5: - resolution: {integrity: sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==} - engines: {node: '>=16.11.0'} - dependencies: - '@discordjs/formatters': 0.3.2 - '@discordjs/util': 1.0.1 - '@sapphire/shapeshift': 3.9.2 - discord-api-types: 0.37.50 - fast-deep-equal: 3.1.3 - ts-mixer: 6.0.3 - tslib: 2.6.2 - dev: false - - /@discordjs/collection@1.5.3: - resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} - engines: {node: '>=16.11.0'} - dev: false - - /@discordjs/formatters@0.3.2: - resolution: {integrity: sha512-lE++JZK8LSSDRM5nLjhuvWhGuKiXqu+JZ/DsOR89DVVia3z9fdCJVcHF2W/1Zxgq0re7kCzmAJlCMMX3tetKpA==} - engines: {node: '>=16.11.0'} - dependencies: - discord-api-types: 0.37.50 - dev: false - - /@discordjs/rest@2.0.1: - resolution: {integrity: sha512-/eWAdDRvwX/rIE2tuQUmKaxmWeHmGealttIzGzlYfI4+a7y9b6ZoMp8BG/jaohs8D8iEnCNYaZiOFLVFLQb8Zg==} - engines: {node: '>=16.11.0'} - dependencies: - '@discordjs/collection': 1.5.3 - '@discordjs/util': 1.0.1 - '@sapphire/async-queue': 1.5.0 - '@sapphire/snowflake': 3.5.1 - '@vladfrangu/async_event_emitter': 2.2.2 - discord-api-types: 0.37.50 - magic-bytes.js: 1.0.15 - tslib: 2.6.2 - undici: 5.22.1 - dev: false - - /@discordjs/util@1.0.1: - resolution: {integrity: sha512-d0N2yCxB8r4bn00/hvFZwM7goDcUhtViC5un4hPj73Ba4yrChLSJD8fy7Ps5jpTLg1fE9n4K0xBLc1y9WGwSsA==} - engines: {node: '>=16.11.0'} - dev: false - - /@discordjs/ws@1.0.1: - resolution: {integrity: sha512-avvAolBqN3yrSvdBPcJ/0j2g42ABzrv3PEL76e3YTp2WYMGH7cuspkjfSyNWaqYl1J+669dlLp+YFMxSVQyS5g==} - engines: {node: '>=16.11.0'} - dependencies: - '@discordjs/collection': 1.5.3 - '@discordjs/rest': 2.0.1 - '@discordjs/util': 1.0.1 - '@sapphire/async-queue': 1.5.0 - '@types/ws': 8.5.5 - '@vladfrangu/async_event_emitter': 2.2.2 - discord-api-types: 0.37.50 - tslib: 2.6.2 - ws: 8.13.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: false - - /@paperdave/logger@3.0.1: - resolution: {integrity: sha512-sLl/oM6U5KUU1bLzjDzshsJnrlGEPT7J0BFMqYr6HzX0RJbsKgRf4XRJgXsusT+rFDfOSHrJAootuImlt8q/9A==} - engines: {node: '>=16'} - dependencies: - '@paperdave/utils': 1.8.0 - ansi-escapes: 6.2.0 - chalk: 5.3.0 - strip-ansi: 7.1.0 - dev: false - - /@paperdave/utils@1.8.0: - resolution: {integrity: sha512-/1ckfqC0migr4GjtFhpqNUIgPibign4kRMmEzGsZ6hilvQ0cbL8Hu96ZzvM/matlOK4tOeEj9aH4f1Bip8AH1A==} - engines: {node: '>=16'} - dependencies: - utility-types: 3.10.0 - yaml: 2.3.1 - dev: false - - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: false - optional: true - - /@sapphire/async-queue@1.5.0: - resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - dev: false - - /@sapphire/shapeshift@3.9.2: - resolution: {integrity: sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - dependencies: - fast-deep-equal: 3.1.3 - lodash: 4.17.21 - dev: false - - /@sapphire/snowflake@3.5.1: - resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - dev: false - - /@types/node@20.5.3: - resolution: {integrity: sha512-ITI7rbWczR8a/S6qjAW7DMqxqFMjjTo61qZVWJ1ubPvbIQsL5D/TvwjYEalM8Kthpe3hTzOGrF2TGbAu2uyqeA==} - requiresBuild: true - dev: false - - /@types/ws@8.5.5: - resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} - requiresBuild: true - dependencies: - '@types/node': 20.5.3 - dev: false - - /@vladfrangu/async_event_emitter@2.2.2: - resolution: {integrity: sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - dev: false - - /algoliasearch@4.19.1: - resolution: {integrity: sha512-IJF5b93b2MgAzcE/tuzW0yOPnuUyRgGAtaPv5UUywXM8kzqfdwZTO4sPJBzoGz1eOy6H9uEchsJsBFTELZSu+g==} - dependencies: - '@algolia/cache-browser-local-storage': 4.19.1 - '@algolia/cache-common': 4.19.1 - '@algolia/cache-in-memory': 4.19.1 - '@algolia/client-account': 4.19.1 - '@algolia/client-analytics': 4.19.1 - '@algolia/client-common': 4.19.1 - '@algolia/client-personalization': 4.19.1 - '@algolia/client-search': 4.19.1 - '@algolia/logger-common': 4.19.1 - '@algolia/logger-console': 4.19.1 - '@algolia/requester-browser-xhr': 4.19.1 - '@algolia/requester-common': 4.19.1 - '@algolia/requester-node-http': 4.19.1 - '@algolia/transporter': 4.19.1 - dev: false - - /ansi-escapes@6.2.0: - resolution: {integrity: sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==} - engines: {node: '>=14.16'} - dependencies: - type-fest: 3.13.1 - dev: false - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: false - - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: false - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: false - - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - dev: false - - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - dev: false - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: false - - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 - dev: false - - /bun-types@0.7.3: - resolution: {integrity: sha512-kssLD5mDLoawmLZFgQRRq0Wy+dca/os6TZ0MHWyFVoVAEwSrpAxmNCZ1K1GUelfhlDaL2FikRxeF9GkATdzXZg==} - dev: true - - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: false - - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: false - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: false - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: false - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: false - - /discord-api-types@0.37.50: - resolution: {integrity: sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==} - dev: false - - /discord.js@14.13.0: - resolution: {integrity: sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==} - engines: {node: '>=16.11.0'} - dependencies: - '@discordjs/builders': 1.6.5 - '@discordjs/collection': 1.5.3 - '@discordjs/formatters': 0.3.2 - '@discordjs/rest': 2.0.1 - '@discordjs/util': 1.0.1 - '@discordjs/ws': 1.0.1 - '@sapphire/snowflake': 3.5.1 - '@types/ws': 8.5.5 - discord-api-types: 0.37.50 - fast-deep-equal: 3.1.3 - lodash.snakecase: 4.1.1 - tslib: 2.6.2 - undici: 5.22.1 - ws: 8.13.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: false - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: false - - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: false - - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: false - - /extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - dependencies: - is-extendable: 0.1.1 - dev: false - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: false - - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - dev: false - - /glob@10.3.3: - resolution: {integrity: sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.0 - minimatch: 9.0.3 - minipass: 7.0.3 - path-scurry: 1.10.1 - dev: false - - /gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - dev: false - - /is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - dev: false - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: false - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: false - - /jackspeak@2.3.0: - resolution: {integrity: sha512-uKmsITSsF4rUWQHzqaRUuyAir3fZfW3f202Ee34lz/gZCi970CPZwyQXLGNgWJvvZbvFyzeyGq0+4fcG/mBKZg==} - engines: {node: '>=14'} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - dev: false - - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: false - - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: false - - /lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - dev: false - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: false - - /lru-cache@10.0.1: - resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} - engines: {node: 14 || >=16.14} - dev: false - - /magic-bytes.js@1.0.15: - resolution: {integrity: sha512-bpRmwbRHqongRhA+mXzbLWjVy7ylqmfMBYaQkSs6pac0z6hBTvsgrH0r4FBYd/UYVJBmS6Rp/O+oCCQVLzKV1g==} - dev: false - - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - brace-expansion: 2.0.1 - dev: false - - /minipass@7.0.3: - resolution: {integrity: sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==} - engines: {node: '>=16 || 14 >=14.17'} - dev: false - - /nan@2.17.0: - resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==} - dev: false - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: false - - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - lru-cache: 10.0.1 - minipass: 7.0.3 - dev: false - - /section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - dev: false - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: false - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: false - - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: false - - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: false - - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: false - - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - dev: false - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: false - - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - dev: false - - /strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - dev: false - - /ts-mixer@6.0.3: - resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} - dev: false - - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: false - - /type-fest@3.13.1: - resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} - engines: {node: '>=14.16'} - dev: false - - /undici@5.22.1: - resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} - engines: {node: '>=14.0'} - dependencies: - busboy: 1.6.0 - dev: false - - /utility-types@3.10.0: - resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} - engines: {node: '>= 4'} - dev: false - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: false - - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: false - - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - dev: false - - /ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - - /yaml@2.3.1: - resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} - engines: {node: '>= 14'} - dev: false - - /zlib-sync@0.1.8: - resolution: {integrity: sha512-Xbu4odT5SbLsa1HFz8X/FvMgUbJYWxJYKB2+bqxJ6UOIIPaVGrqHEB3vyXDltSA6tTqBhSGYLgiVpzPQHYi3lA==} - requiresBuild: true - dependencies: - nan: 2.17.0 - dev: false diff --git a/scripts/validate_tags/bun.lockb b/scripts/validate_tags/bun.lockb deleted file mode 100755 index 69e6960..0000000 Binary files a/scripts/validate_tags/bun.lockb and /dev/null differ diff --git a/scripts/validate_tags/index.ts b/scripts/validate_tags/index.ts new file mode 100644 index 0000000..106a202 --- /dev/null +++ b/scripts/validate_tags/index.ts @@ -0,0 +1,121 @@ +import type { Tag } from "../../src/structs/Tag.js"; +import * as matter from "gray-matter"; +import { join, dirname } from "node:path"; +import { readFileSync } from "node:fs"; +import { Glob } from "bun"; + +const githubToken = process.env["github-token"]; +const commitSha = process.env["commit-sha"]; +const pullRequestNumber = process.env["pr-number"]; + +const urlRegex = + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/gi; + +// Check if files/tags.toml was changed +const files = (await Bun.file("./files.json").json()) as string[]; +if (!files.some((f) => f.includes("tags"))) process.exit(0); + +const errors: string[] = []; + +const tags: Tag[] = []; +const tagPaths = Array.from( + new Glob( + join(import.meta.dir, "..", "..", "..", "data", "tags", "*.md") + ).scanSync() +); +for (const tagPath of tagPaths) { + const content = readFileSync(tagPath); + const frontMatter = matter(content); + + tags.push({ + name: dirname(tagPath), + question: frontMatter.data.question, + keywords: frontMatter.data.keywords, + answer: frontMatter.content, + category_ids: frontMatter.data.category_ids ?? null, + channel_ids: frontMatter.data.channel_ids ?? null, + }); +} + +try { + await requestGithub(`issues/${pullRequestNumber}/labels`, { + labels: ["tags"], + }); +} catch (e) { + errors.push(e.message); +} + +for (const tag of tags) { + const key = tag.name; + + if (!tag?.keywords || tag.keywords.length === 0) + errors.push(`**[${key}]:** Tag must have keywords`); + if (tag?.keywords?.[0] !== key) + errors.push( + `**[${key}]:** First keyword of tag is not the same as the tag name` + ); + if (!tag.answer) errors.push(`**[${key}]:** Tag must have content`); + + if (tag.answer) { + for (const url of tag.answer.match(urlRegex) || []) { + const firstChar = tag.answer.split(url)[0].slice(-1); + const lastChar = tag.answer.split(url)[1].slice(0, 1); + if (firstChar !== "<" || lastChar !== ">") + errors.push(`**[${key}]:** Link must be wrapped in <>`); + } + } + + if (tag.keywords) { + const keywords = [...new Set(tag.keywords)]; + if (keywords.length !== tag.keywords.length) + errors.push(`**[${key}]:** Keywords must be unique`); + } +} + +if (errors.length === 0) { + await requestGithub(`pulls/${pullRequestNumber}/reviews`, { + commit_id: commitSha, + event: "APPROVE", + }); + + await requestGithub(`pulls/${pullRequestNumber}/requested_reviewers`, { + reviewers: ["xHyroM"], + }); + + await requestGithub( + `issues/${pullRequestNumber}/labels/waiting`, + {}, + "DELETE" + ); + + await requestGithub(`issues/${pullRequestNumber}/labels`, { + labels: ["ready"], + }); +} else { + await requestGithub(`pulls/${pullRequestNumber}/reviews`, { + commit_id: commitSha, + body: "### Please fix the following problems:\n" + errors.join("\n"), + event: "REQUEST_CHANGES", + }); + + await requestGithub(`issues/${pullRequestNumber}/labels`, { + labels: ["waiting"], + }); +} + +async function requestGithub( + url: string, + body: any, + method?: "POST" | "DELETE" +) { + await fetch(`https://api.github.com/repos/xHyroM/bun-discord-bot/${url}`, { + method: method || "POST", + headers: { + Accept: "application/vnd.github+json", + Authorization: `token ${githubToken}`, + }, + body: JSON.stringify(body), + }); +} + +export {}; diff --git a/scripts/validate_tags/package.json b/scripts/validate_tags/package.json deleted file mode 100644 index e7cd61a..0000000 --- a/scripts/validate_tags/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "validate_tags", - "scripts": { - "start": "bun src/index.ts" - }, - "dependencies": { - "glob": "^10.3.3" - } -} diff --git a/scripts/validate_tags/src/index.ts b/scripts/validate_tags/src/index.ts deleted file mode 100644 index e72db3e..0000000 --- a/scripts/validate_tags/src/index.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { Tag } from "../../../src/structs/Tag.ts"; -import { globSync as glob } from "glob"; -import * as matter from "gray-matter"; -import { join, dirname } from "node:path"; -import { readFileSync } from "node:fs"; - -const githubToken = process.env['github-token']; -const commitSha = process.env['commit-sha']; -const pullRequestNumber = process.env['pr-number']; - -const urlRegex = - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/gi; - -// Check if files/tags.toml was changed -const files = await Bun.file('./files.json').json() as string[]; -if (!files.some(f => f.includes("tags"))) process.exit(0); - -const errors = []; - -const tags: Tag[] = []; -const tagPaths = glob(join(__dirname, "..", "..", "..", "data", "tags", "*.md")); -for (const tagPath of tagPaths) { - const content = readFileSync(tagPath); - const frontMatter = matter(content); - - tags.push({ - name: dirname(tagPath), - question: frontMatter.data.question, - keywords: frontMatter.data.keywords, - answer: frontMatter.content, - category_ids: frontMatter.data.category_ids ?? null, - channel_ids: frontMatter.data.channel_ids ?? null, - }); -} - -try { - await requestGithub(`issues/${pullRequestNumber}/labels`, { - labels: ['tags'], - }); -} catch (e) { - errors.push(e.message); -} - -for (const tag of tags) { - const key = tag.name; - - if (!tag?.keywords || tag.keywords.length === 0) - errors.push(`**[${key}]:** Tag must have keywords`); - if (tag?.keywords?.[0] !== key) - errors.push( - `**[${key}]:** First keyword of tag is not the same as the tag name` - ); - if (!tag.answer) errors.push(`**[${key}]:** Tag must have content`); - - if (tag.answer) { - for (const url of tag.answer.match(urlRegex) || []) { - const firstChar = tag.answer.split(url)[0].slice(-1); - const lastChar = tag.answer.split(url)[1].slice(0, 1); - if (firstChar !== '<' || lastChar !== '>') - errors.push(`**[${key}]:** Link must be wrapped in <>`); - } - } - - if (tag.keywords) { - const keywords = [...new Set(tag.keywords)]; - if (keywords.length !== tag.keywords.length) - errors.push(`**[${key}]:** Keywords must be unique`); - } -} - -if (errors.length === 0) { - await requestGithub(`pulls/${pullRequestNumber}/reviews`, { - commit_id: commitSha, - event: 'APPROVE', - }); - - await requestGithub(`pulls/${pullRequestNumber}/requested_reviewers`, { - reviewers: ['xHyroM'], - }); - - await requestGithub( - `issues/${pullRequestNumber}/labels/waiting`, - {}, - 'DELETE' - ); - - await requestGithub(`issues/${pullRequestNumber}/labels`, { - labels: ['ready'], - }); -} else { - await requestGithub(`pulls/${pullRequestNumber}/reviews`, { - commit_id: commitSha, - body: '### Please fix the following problems:\n' + errors.join('\n'), - event: 'REQUEST_CHANGES', - }); - - await requestGithub(`issues/${pullRequestNumber}/labels`, { - labels: ['waiting'], - }); -} - -async function requestGithub( - url: string, - body: any, - method?: 'POST' | 'DELETE' -) { - await fetch(`https://api.github.com/repos/xHyroM/bun-discord-bot/${url}`, { - method: method || 'POST', - headers: { - Accept: 'application/vnd.github+json', - Authorization: `token ${githubToken}`, - }, - body: JSON.stringify(body), - }); -} - -export {}; diff --git a/scripts/validate_tags/tsconfig.json b/scripts/validate_tags/tsconfig.json deleted file mode 100644 index 4082f16..0000000 --- a/scripts/validate_tags/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../tsconfig.json" -} diff --git a/scripts/validate_tags/src/types.d.ts b/scripts/validate_tags/types.d.ts similarity index 100% rename from scripts/validate_tags/src/types.d.ts rename to scripts/validate_tags/types.d.ts diff --git a/src/commands/docs.ts b/src/commands/docs.ts deleted file mode 100644 index c9816b1..0000000 --- a/src/commands/docs.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { SlashCommandStringOption, SlashCommandUserOption } from "discord.js"; -import { defineCommand } from "../loaders/commands.ts"; -import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts"; -import { InteractionCommandContext } from "../structs/context/CommandContext.ts"; -import algoliasearch from "algoliasearch"; - -const algoliaClient = algoliasearch("2527C13E0N", "4efc87205e1fce4a1f267cadcab42cb2"); -const algoliaIndex = algoliaClient.initIndex("bun"); - -defineCommand({ - name: "docs", - description: "Search at docs", - options: [ - { - ...new SlashCommandStringOption() - .setName("query") - .setRequired(true) - .setAutocomplete(true) - .setDescription("Select query") - .toJSON(), - run: async(context: AutocompleteContext) => { - const query = context.options.getString("query"); - const result = await algoliaIndex.search(query, { - hitsPerPage: 25, - }); - - return context.respond( - result.hits.map(hit => { - const name = getHitName(hit); - - return { - name: name.full.length > 100 ? name.full.slice(0, 100) : name.full, - value: name.name.length > 100 ? name.name.slice(0, 100) : name.name, - } - }) - ); - }, - }, - { - ...new SlashCommandUserOption() - .setName("target") - .setRequired(false) - .setDescription("User to mention") - .toJSON() - } - ], - run: async(context: InteractionCommandContext) => { - await context.interaction.deferReply(); - - const query = context.interaction.options.getString("query"); - const target = context.interaction.options.getUser("target"); - - const result = await algoliaIndex.search(query, { - hitsPerPage: 1, - }); - - const hit = result.hits[0]; - // @ts-expect-error exist - const url = hit.url; - const name = getHitName(hit); - // @ts-expect-error can exist - const snippetContent = hit._snippetResult?.content?.value?.replace(/<[^>]+>/g, ""); - // @ts-expect-error can exist - const notice = hit.content?.replace(/\r/g, ""); - - const content = [ - target ? `*Suggestion for <@${target.id}>:*\n` : "", - `[*${name.full}*](<${url}>)`, - snippetContent ? snippetContent : "", - notice ? notice.split("\n").map(s => `> ${s}`).join("\n") : "" - ].join("\n") - - await context.interaction.editReply({ - content, - allowedMentions: { - parse: [ "users" ], - } - }); - } -}) - -function getHitName(hit) { - const type = hit.hierarchy.lvl0 === "Documentation" ? "📖" : "🗺️"; - const hierarchy = Object.values(hit.hierarchy).filter(v => v); - hierarchy.shift(); - - const name = hierarchy.join(" > "); - - return { - full: `${type} ${name}`, - name: name, - emoji: type, - } -} diff --git a/src/commands/docs.tsx b/src/commands/docs.tsx new file mode 100644 index 0000000..cd251a5 --- /dev/null +++ b/src/commands/docs.tsx @@ -0,0 +1,94 @@ +import { ApplicationCommand, StringOption, UserOption } from "@lilybird/jsx"; +import { SlashCommand } from "@lilybird/handlers"; +import algoliasearch from "algoliasearch"; + +// @ts-expect-error It is callable, but algolia for some reason has a namespace with the same name +const algoliaClient = algoliasearch( + "2527C13E0N", + "4efc87205e1fce4a1f267cadcab42cb2" +); +const algoliaIndex = algoliaClient.initIndex("bun"); + +export default { + post: "GLOBAL", + data: ( + + + + + ), + autocomplete: async (interaction) => { + const query = interaction.data.getFocused().value; + const result = await algoliaIndex.search(query, { + hitsPerPage: 25, + }); + + return interaction.respond( + result.hits.map((hit: any) => { + const name = getHitName(hit); + + return { + name: name.full.length > 100 ? name.full.slice(0, 100) : name.full, + value: name.name.length > 100 ? name.name.slice(0, 100) : name.name, + }; + }) + ); + }, + run: async (interaction) => { + await interaction.deferReply(); + + const query = interaction.data.getString("query"); + const target = interaction.data.getUser("target"); + + const result = await algoliaIndex.search(query, { + hitsPerPage: 1, + }); + + const hit = result.hits[0]; + const url = hit.url; + const name = getHitName(hit); + const snippetContent = hit._snippetResult?.content?.value?.replace( + /<[^>]+>/g, + "" + ); + const notice = hit.content?.replace(/\r/g, ""); + + const content = [ + target ? `*Suggestion for <@${target}>:*\n` : "", + `[*${name.full}*](<${url}>)`, + snippetContent ? snippetContent : "", + notice + ? notice + .split("\n") + .map((s: any) => `> ${s}`) + .join("\n") + : "", + ].join("\n"); + + await interaction.editReply({ + content, + // allowedMentions: { + // parse: ["users"], + // } + }); + }, +} satisfies SlashCommand; + +function getHitName(hit: any) { + const type = hit.hierarchy.lvl0 === "Documentation" ? "📖" : "🗺️"; + const hierarchy = Object.values(hit.hierarchy).filter((v) => v); + hierarchy.shift(); + + const name = hierarchy.join(" > "); + + return { + full: `${type} ${name}`, + name: name, + emoji: type, + }; +} diff --git a/src/commands/github.ts b/src/commands/github.ts deleted file mode 100644 index 7f60ef3..0000000 --- a/src/commands/github.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { SlashCommandBooleanOption, SlashCommandStringOption, time } from "discord.js"; -import { defineCommand } from "../loaders/commands.ts"; -import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts"; -import { InteractionCommandContext } from "../structs/context/CommandContext.ts"; -import { safeSlice, silently } from "../util.ts"; - -type State = "open" | "closed_as_completed" | "closed_as_not_planned" | "closed" | "merged" | "draft" | "all"; -type StateEmoji = "🔴" | "🟠" | "🟢" | "⚫️" | "⚪️" | "🟣" | "📝"; -type Type = "issues" | "pull_requests" | "both"; - -interface Item { - html_url: string; - number: number; - title: string; - user: { - html_url: string; - login: string; - } - emoji: { - state: StateEmoji; - type: string; - }; - created_at: Date | null; - closed_at: Date | null; - pull_request?: { - merged_at: Date | null; - } -} - -defineCommand({ - name: "github", - description: "Query an issue, pull request or direct link to issue, pull request", - options: [ - { - ...new SlashCommandStringOption() - .setName("query") - .setDescription("Issue/Pull request number or name") - .setRequired(true) - .setAutocomplete(true) - .setMaxLength(100), - run: async(ctx: AutocompleteContext) => { - const query = ctx.options.getString("query"); - const state: State = ctx.options.getString("state") as State || "all"; - const type: Type = ctx.options.getString("type") as Type || "both"; - - const response = await search(query, state, type, 25); - - await silently(ctx.respond(response.map(r => ({ - name: safeSlice(`${r.emoji.type} ${r.emoji.state} #${r.number} | ${r.title}`, 100), - value: r.number.toString() - })))); - } - }, - { - ...new SlashCommandStringOption() - .setName("state") - .setDescription("Issue or Pull request state") - .setRequired(false) - .addChoices( - { - name: "🔴🟠 Open", - value: "open" - }, - { - name: "🟢 Closed as completed", - value: "closed_as_completed" - }, - { - name: "⚪️ Closed as not planned", - value: "closed_as_not_planned" - }, - { - name: "⚫️ Closed", - value: "closed" - }, - { - name: "🟣 Merged", - value: "merged" - }, - { - name: "📝 Draft", - value: "draft", - }, - { - name: "🌍 All", - value: "all", - } - ) - }, - { - ...new SlashCommandStringOption() - .setName("type") - .setDescription("Issue or Pull Requests") - .setRequired(false) - .addChoices( - { - name: "🐛 Issues", - value: "issues" - }, - { - name: "🔨 Pull Requests", - value: "pull_requests" - }, - { - name: "🌍 Both", - value: "both" - } - ) - }, - { - ...new SlashCommandBooleanOption() - .setName("hide") - .setDescription("Show this message only for you") - .setRequired(false) - } - ], - run: async(ctx: InteractionCommandContext) => { - const hide = ctx.interaction.options.getBoolean("hide") ?? false; - - await ctx.interaction.deferReply({ - ephemeral: hide - }); - - const query = ctx.interaction.options.getString("query"); - const state: State = ctx.interaction.options.getString("state") as State || "all"; - const type: Type = ctx.interaction.options.getString("type") as Type || "both"; - - const result = (await search(query, state, type))[0]; - if (!result) { - ctx.interaction.editReply({ - content: `❌ Couldn't find issue or pull request \`${query}\`` - }); - return; - } - - ctx.interaction.editReply({ - content: [ - `${result.emoji.type} ${result.emoji.state} [#${result.number} in oven-sh/bun](<${result.html_url}>) by [${result.user.login}](<${result.user.html_url}>) ${stateToText(result)} ${stateToTimestamp(result)}`, - result.title - ].join("\n") - }) - } -}); - -function stateToText(item: Item) { - switch (item.emoji.state) { - case "🔴": - case "🟠": - case "📝": { - return "opened"; - } - case "🟢": { - return "closed as completed"; - } - case "⚪️": { - return "closed as not planned"; - } - case "⚫️": { - return "closed"; - } - case "🟣": { - return "merged"; - } - } -} - -function stateToTimestamp(item: Item) { - let timestamp: Date; - - switch (item.emoji.state) { - case "🔴": - case "🟠": - case "📝": { - timestamp = item.created_at; - break; - } - case "🟢": - case "⚪️": - case "⚫️": { - timestamp = item.closed_at; - break; - } - case "🟣": { - timestamp = item.pull_request.merged_at; - break; - } - } - - return ``; -} - -async function search(query: string, state: State, type: Type, length = 1): Promise { - let actualQuery = "repo:oven-sh/bun "; - - switch (state) { - case "open": { - actualQuery += "state:open "; - break; - } - case "closed": { - actualQuery += "state:closed "; - break; - } - case "closed_as_completed": { - actualQuery += "state:closed reason:completed " - break; - } - case "closed_as_not_planned": { - actualQuery += "state:closed reason:\"not planned\" "; - break; - } - case "merged": { - actualQuery += "is:merged "; - break; - } - case "draft": { - actualQuery += "draft:true "; - break; - } - } - - switch (type) { - case "issues": { - actualQuery += "type:issue " - break; - } - case "pull_requests": { - actualQuery += "type:pr " - break; - } - } - - // append user query + remove all tags - actualQuery += query.replace(/\S+:\S+/g, "").trim(); - - const response = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(actualQuery)}&per_page=${length}`, { - headers: { - "Authorization": `Bearer ${process.env.GITHUB_TOKEN}`, - "Accept": "application/vnd.github+json" - } - }); - - const body = await response.json(); - const items = body.items; - - return items.map(item => { - let state = ""; - if (item.state === "closed") { - if (item.pull_request) { - state = item.pull_request.merged_at ? "🟣" : "⚫️"; - } else { - state = item.state_reason === "completed" ? "🟢" : "⚪️"; - } - } else { - if (item.pull_request) { - state = item.draft ? "📝" : "🟠"; - } else { - state = "🔴"; - } - } - - const base = { - ...item, - emoji: { - state: state, - type: item.pull_request ? "🔨" : "🐛" - }, - created_at: new Date(item.created_at), - closed_at: item.closed_at ? new Date(item.cloased_at) : null - } - - if (item.pull_request) { - base.pull_request = {}; - base.pull_request.merged_at = item.pull_request.merged_at ? new Date(item.pull_request.merged_at) : null; - } - - return base; - }); -} diff --git a/src/commands/github.tsx b/src/commands/github.tsx new file mode 100644 index 0000000..eaf24e0 --- /dev/null +++ b/src/commands/github.tsx @@ -0,0 +1,275 @@ +import { + ApplicationCommand, + BooleanOption, + CommandOptions, + StringOption, +} from "@lilybird/jsx"; +import { SlashCommand } from "@lilybird/handlers"; +import { safeSlice, silently } from "../util.ts"; + +type State = + | "open" + | "closed_as_completed" + | "closed_as_not_planned" + | "closed" + | "merged" + | "draft" + | "all"; +type StateEmoji = "🔴" | "🟠" | "🟢" | "⚫️" | "⚪️" | "🟣" | "📝"; +type Type = "issues" | "pull_requests" | "both"; + +interface Item { + html_url: string; + number: number; + title: string; + user: { + html_url: string; + login: string; + }; + emoji: { + state: StateEmoji; + type: string; + }; + created_at: Date | null; + closed_at: Date | null; + pull_request?: { + merged_at: Date | null; + }; +} + +export default { + post: "GLOBAL", + data: ( + + + + + + + + + + + + + + + + + + + ), + run: async (interaction) => { + const hide = interaction.data.getBoolean("hide") ?? false; + + await interaction.deferReply(hide); + + const query = interaction.data.getString("query", true); + const state: State = + (interaction.data.getString("state") as State) || "all"; + const type: Type = (interaction.data.getString("type") as Type) || "both"; + + const result = (await search(query, state, type))[0]; + if (!result) { + interaction.editReply({ + content: `❌ Couldn't find issue or pull request \`${query}\``, + }); + return; + } + + interaction.editReply({ + content: [ + `${result.emoji.type} ${result.emoji.state} [#${ + result.number + } in oven-sh/bun](<${result.html_url}>) by [${result.user.login}](<${ + result.user.html_url + }>) ${stateToText(result)} ${stateToTimestamp(result)}`, + result.title, + ].join("\n"), + }); + }, + + autocomplete: async (interaction) => { + const query = interaction.data.getFocused().value; + const state: State = + (interaction.data.getString("state") as State) || "all"; + const type: Type = (interaction.data.getString("type") as Type) || "both"; + + const response = await search(query, state, type, 25); + + await silently( + interaction.respond( + response.map((r) => ({ + name: safeSlice( + `${r.emoji.type} ${r.emoji.state} #${r.number} | ${r.title}`, + 100 + ), + value: r.number.toString(), + })) + ) + ); + }, +} satisfies SlashCommand; + +function stateToText(item: Item) { + switch (item.emoji.state) { + case "🔴": + case "🟠": + case "📝": { + return "opened"; + } + case "🟢": { + return "closed as completed"; + } + case "⚪️": { + return "closed as not planned"; + } + case "⚫️": { + return "closed"; + } + case "🟣": { + return "merged"; + } + } +} + +function stateToTimestamp(item: Item) { + let timestamp: Date; + + switch (item.emoji.state) { + case "🔴": + case "🟠": + case "📝": { + timestamp = item.created_at as Date; + break; + } + case "🟢": + case "⚪️": + case "⚫️": { + timestamp = item.closed_at as Date; + break; + } + case "🟣": { + timestamp = item.pull_request?.merged_at as Date; + break; + } + } + + return ``; +} + +async function search( + query: string, + state: State, + type: Type, + length = 1 +): Promise { + let actualQuery = "repo:oven-sh/bun "; + + switch (state) { + case "open": { + actualQuery += "state:open "; + break; + } + case "closed": { + actualQuery += "state:closed "; + break; + } + case "closed_as_completed": { + actualQuery += "state:closed reason:completed "; + break; + } + case "closed_as_not_planned": { + actualQuery += 'state:closed reason:"not planned" '; + break; + } + case "merged": { + actualQuery += "is:merged "; + break; + } + case "draft": { + actualQuery += "draft:true "; + break; + } + } + + switch (type) { + case "issues": { + actualQuery += "type:issue "; + break; + } + case "pull_requests": { + actualQuery += "type:pr "; + break; + } + } + + // append user query + remove all tags + actualQuery += query.replace(/\S+:\S+/g, "").trim(); + + const response = await fetch( + `https://api.github.com/search/issues?q=${encodeURIComponent( + actualQuery + )}&per_page=${length}`, + { + headers: { + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + Accept: "application/vnd.github+json", + }, + } + ); + + const body = await response.json(); + const items = body.items; + + return items.map((item: any) => { + let state = ""; + if (item.state === "closed") { + if (item.pull_request) { + state = item.pull_request.merged_at ? "🟣" : "⚫️"; + } else { + state = item.state_reason === "completed" ? "🟢" : "⚪️"; + } + } else { + if (item.pull_request) { + state = item.draft ? "📝" : "🟠"; + } else { + state = "🔴"; + } + } + + const base = { + ...item, + emoji: { + state: state, + type: item.pull_request ? "🔨" : "🐛", + }, + created_at: new Date(item.created_at), + closed_at: item.closed_at ? new Date(item.cloased_at) : null, + }; + + if (item.pull_request) { + base.pull_request = {}; + base.pull_request.merged_at = item.pull_request.merged_at + ? new Date(item.pull_request.merged_at) + : null; + } + + return base; + }); +} diff --git a/src/commands/index.ts b/src/commands/index.ts deleted file mode 100644 index aa6cad5..0000000 --- a/src/commands/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "./version.ts"; -import "./docs.ts"; -import "./tag.ts"; -import "./ping.ts"; -import "./github.ts"; - -import { registerCommands } from "../loaders/commands.ts"; -await registerCommands(); diff --git a/src/commands/ping.ts b/src/commands/ping.ts deleted file mode 100644 index 39e4683..0000000 --- a/src/commands/ping.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { defineCommand } from "../loaders/commands.ts"; -import { Bubu } from "../structs/Client.ts"; -import { InteractionCommandContext, MessageCommandContext } from "../structs/context/CommandContext.ts"; - -defineCommand({ - name: "ping", - description: "pong", - run: async(ctx: InteractionCommandContext) => { - const message = await ctx.interaction.deferReply({ - ephemeral: true, - }); - - const restPing = message.createdTimestamp - ctx.interaction.createdTimestamp; - - ctx.interaction.editReply({ - content: `🏓 WebSocket: \`${Bubu.ws.ping}ms\` | Rest: \`${restPing}ms\`` - }); - }, - runMessage: async(ctx: MessageCommandContext) => { - const message = await ctx.reply({ - content: "🏓...", - }); - - const restPing = message.createdTimestamp - ctx.message.createdTimestamp; - - message.edit({ - content: `🏓 WebSocket: \`${Bubu.ws.ping}ms\` | Rest: \`${restPing}ms\`` - }); - } -}) diff --git a/src/commands/ping.tsx b/src/commands/ping.tsx new file mode 100644 index 0000000..922c986 --- /dev/null +++ b/src/commands/ping.tsx @@ -0,0 +1,16 @@ +import { ApplicationCommand } from "@lilybird/jsx"; +import { SlashCommand } from "@lilybird/handlers"; + +export default { + post: "GLOBAL", + data: , + run: async (interaction) => { + await interaction.deferReply(); + + const { ws, rest } = await interaction.client.ping(); + + await interaction.editReply({ + content: `🏓 WebSocket: \`${ws}ms\` | Rest: \`${rest}ms\``, + }); + }, +} satisfies SlashCommand; diff --git a/src/commands/tag.ts b/src/commands/tag.ts deleted file mode 100644 index a9a3ae0..0000000 --- a/src/commands/tag.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { GuildTextBasedChannel, SlashCommandStringOption, SlashCommandUserOption, User } from "discord.js"; -import { defineCommand } from "../loaders/commands.ts"; -import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts"; -import { getTags, searchTag } from "../loaders/tags.ts"; -import { InteractionCommandContext, MessageCommandContext } from "../structs/context/CommandContext.ts"; -import { Bubu } from "../structs/Client.ts"; - -defineCommand({ - name: "tag", - description: "Get tag", - options: [ - { - ...new SlashCommandStringOption() - .setName("query") - .setRequired(true) - .setAutocomplete(true) - .setDescription("Select query") - .toJSON(), - run: async(context: AutocompleteContext) => { - const query = context.options.getString("query"); - if (!query) { - return context.respond(getTags(context.channel, 25)); - } - - const tags = searchTag(context.channel, query, true); - if (tags.length > 0) - return context.respond(tags); - - return context.respond(getTags(context.channel, 25)); - }, - }, - { - ...new SlashCommandUserOption() - .setName("target") - .setRequired(false) - .setDescription("User to mention") - .toJSON() - } - ], - run: (ctx: InteractionCommandContext) => { - const query = ctx.interaction.options.getString("query"); - const target = ctx.interaction.options.getUser("target"); - - const tag = searchTag(ctx.channel, query, false); - if (!tag) { - return ctx.reply({ - content: `\`❌\` Could not find a tag \`${query}\``, - ephemeral: true, - }); - } - - ctx.reply({ - content: [ - target ? `*Suggestion for <@${target.id}>:*\n` : "", - `**${tag.question}**`, - tag.answer - ].join("\n"), - allowedMentions: { - parse: [ "users" ] - } - }); - }, - runMessage: (ctx: MessageCommandContext) => { - if (!ctx.message.inGuild()) return; - - const keyword = ctx.options?.[0] ?? "what-is-bun"; - - const target = ctx.options?.[1]?.match(/([0-9]+)/)?.[0]; - const resolvedTarget = target ? Bubu.users.cache.get(target) : null; - - const tag = searchTag(ctx.channel as GuildTextBasedChannel, keyword, false); - if (!keyword || !tag) { - return ctx.reply({ - content: `\`❌\` Could not find a tag \`${keyword}\``, - }); - } - - ctx.reply({ - content: [ - resolvedTarget ? `*Suggestion for <@${resolvedTarget.id}>:*\n` : "", - `**${tag.question}**`, - tag.answer - ].join("\n"), - allowedMentions: { - parse: [ "users" ] - } - }); - } -}) diff --git a/src/commands/tag.tsx b/src/commands/tag.tsx new file mode 100644 index 0000000..91f76bb --- /dev/null +++ b/src/commands/tag.tsx @@ -0,0 +1,54 @@ +import { ApplicationCommand, StringOption, UserOption } from "@lilybird/jsx"; +import { getTags, searchTag } from "../loaders/tags.ts"; +import { SlashCommand } from "@lilybird/handlers"; + +export default { + post: "GLOBAL", + data: ( + + + + + ), + run: async (interaction) => { + if (!interaction.inGuild()) return; + const query = interaction.data.getString("query", true); + const target = interaction.data.getUser("target"); + + const tag = searchTag(interaction.channel, query, false); + if (!tag) { + return interaction.reply({ + content: `\`❌\` Could not find a tag \`${query}\``, + ephemeral: true, + }); + } + + await interaction.reply({ + content: [ + target ? `*Suggestion for <@${target}>:*\n` : "", + `**${tag.question}**`, + tag.answer, + ].join("\n"), + // allowedMentions: { + // parse: ["users"] + // } + }); + }, + autocomplete: async (interaction) => { + if (!interaction.inGuild()) return; + const query = interaction.data.getFocused().value; + if (!query) { + return await interaction.respond(getTags(interaction.channel, 25)); + } + + const tags = searchTag(interaction.channel, query, true); + if (tags.length > 0) return await interaction.respond(tags); + + return await interaction.respond(getTags(interaction.channel, 25)); + }, +} satisfies SlashCommand; diff --git a/src/commands/version.ts b/src/commands/version.ts deleted file mode 100644 index f0f3723..0000000 --- a/src/commands/version.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineCommand } from "../loaders/commands.ts"; -import { COMMIT_HASH, PRODUCTION } from "../constants.ts"; -import { InteractionCommandContext } from "../structs/context/CommandContext.ts"; - -export default defineCommand({ - name: "version", - description: "Show version", - options: [], - run: (context: InteractionCommandContext) => { - context.interaction.reply({ - content: [ - `[git-bun-discord-bot-${COMMIT_HASH}]() ${!PRODUCTION ? "(dev)" : ""}`, - `[v${Bun.version} (${Bun.revision})]()` - ].join("\n"), - ephemeral: true, - }); - } -}); diff --git a/src/commands/version.tsx b/src/commands/version.tsx new file mode 100644 index 0000000..115c902 --- /dev/null +++ b/src/commands/version.tsx @@ -0,0 +1,19 @@ +import { COMMIT_HASH, PRODUCTION } from "../constants.ts"; +import { ApplicationCommand } from "@lilybird/jsx"; +import { SlashCommand } from "@lilybird/handlers"; + +export default { + post: "GLOBAL", + data: , + run: (interaction) => { + interaction.reply({ + content: [ + `[git-bun-discord-bot-${COMMIT_HASH}]() ${ + !PRODUCTION ? "(dev)" : "" + }`, + `[v${Bun.version} (${Bun.revision})]()`, + ].join("\n"), + ephemeral: true, + }); + }, +} satisfies SlashCommand; diff --git a/src/constants.ts b/src/constants.ts index 05e293c..fdcd80a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,11 +1,7 @@ import { spawnSync } from "bun"; export const COMMIT_HASH = spawnSync({ - cmd: [ "git", "log", "--pretty=format:%h", "-n", "1" ] + cmd: ["git", "log", "--pretty=format:%h", "-n", "1"], }).stdout.toString(); export const PRODUCTION = process.env.NODE_ENV === "production"; - -export const MESSAGE_PREFIX = PRODUCTION ? "b" : "<>"; - -export const BUN_ONLY_CHANNEL_ID = "1161157663867027476" diff --git a/src/index.ts b/src/index.ts index e6c0924..a6468d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,28 @@ import "./loaders/tags.ts"; -import "./commands"; -import "./listeners"; -import { Bubu } from "./structs/Client.ts"; +import { createHandler } from "@lilybird/handlers"; +import { createClient, Intents } from "lilybird"; // Make sure bubu will not crash process.on("unhandledRejection", console.error); process.on("uncaughtException", console.error); -Bubu.login(process.env.DISCORD_BOT_TOKEN).catch(console.error); +const listeners = await createHandler({ + prefix: process.env.MESSAGE_PREFIX, + dirs: { + messageCommands: `${import.meta.dir}/message-commands`, + slashCommands: `${import.meta.dir}/commands`, + listeners: `${import.meta.dir}/listeners`, + }, +}); + +await createClient({ + token: process.env.DISCORD_BOT_TOKEN, + intents: [ + Intents.GUILDS, + Intents.GUILD_MESSAGES, + Intents.MESSAGE_CONTENT, + Intents.GUILD_MEMBERS, + ], + ...listeners, +}); diff --git a/src/listeners/index.ts b/src/listeners/index.ts deleted file mode 100644 index f96423d..0000000 --- a/src/listeners/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "./ready.ts"; -import "./interaction_create.ts"; -import "./message_create.ts"; -import "./message_update.ts"; -import "./nickname_moderation.ts"; diff --git a/src/listeners/interaction_create.ts b/src/listeners/interaction_create.ts deleted file mode 100644 index 54ba7a1..0000000 --- a/src/listeners/interaction_create.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Events, Interaction, ChatInputCommandInteraction, AutocompleteInteraction, APIApplicationCommandSubcommandOption, APIApplicationCommandSubcommandGroupOption } from "discord.js"; -import { COMMANDS } from "../loaders/commands.ts"; -import { defineListener } from "../loaders/listeners.ts"; -import { InteractionCommandContext } from "../structs/context/CommandContext.ts"; -import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts"; -import { Option, StringOption } from "../structs/Command.ts"; -import { error } from "@paperdave/logger"; - -defineListener({ - event: Events.InteractionCreate, - run: async(interaction: Interaction) => { - if (interaction.isChatInputCommand()) return await handleCommand(interaction); - if (interaction.isAutocomplete()) return await handleAutocomplete(interaction); - - return; - } -}) - -async function handleCommand(interaction: ChatInputCommandInteraction) { - const command = COMMANDS.get(interaction.commandName); - - if (!command || !command.run) { - interaction.reply({ - content: "Hmm.. Invalid command :P", - ephemeral: true, - }) - return; - } - - const context = new InteractionCommandContext(command, interaction); - await Promise.resolve(command.run(context)) - .catch(async err => { - error(err); - - context.reply({ - content: `Something went wrong... ${err.message} (${err.code})` - }); - }); -} - -async function handleAutocomplete(interaction: AutocompleteInteraction) { - const command = COMMANDS.get(interaction.commandName); - if (!command) return; - - let options = command.options; - - if (interaction.options.getSubcommandGroup(false)) - options = (options.find(o => o.name === interaction.options.getSubcommandGroup()) as APIApplicationCommandSubcommandGroupOption)?.options as Option[]; - - if (interaction.options.getSubcommand(false)) - options = (options.find(o => o.name === interaction.options.getSubcommand()) as APIApplicationCommandSubcommandOption)?.options as Option[]; - - const focused = interaction.options.getFocused(true); - const option = options.find(o => o.name === focused.name) as StringOption; - if (!option) return; - - const context = new AutocompleteContext(option, command, interaction); - await option.run(context); -} diff --git a/src/listeners/member_add.ts b/src/listeners/member_add.ts new file mode 100644 index 0000000..22a3cfe --- /dev/null +++ b/src/listeners/member_add.ts @@ -0,0 +1,9 @@ +import { moderateNick } from "../util.ts"; +import { Event } from "@lilybird/handlers"; + +export default { + event: "guildMemberAdd", + run: (member) => { + moderateNick(member); + }, +} satisfies Event<"guildMemberAdd">; diff --git a/src/listeners/member_update.ts b/src/listeners/member_update.ts new file mode 100644 index 0000000..e403953 --- /dev/null +++ b/src/listeners/member_update.ts @@ -0,0 +1,9 @@ +import { moderateNick } from "../util.ts"; +import { Event } from "@lilybird/handlers"; + +export default { + event: "guildMemberUpdate", + run: (member) => { + moderateNick(member); + }, +} satisfies Event<"guildMemberUpdate">; diff --git a/src/listeners/message_create.ts b/src/listeners/message_create.ts deleted file mode 100644 index 8b4ca09..0000000 --- a/src/listeners/message_create.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Events, Message } from "discord.js"; -import { defineListener } from "../loaders/listeners.ts"; -import { MESSAGE_PREFIX, BUN_ONLY_CHANNEL_ID } from "../constants.ts"; -import { COMMANDS } from "../loaders/commands.ts"; -import { MessageCommandContext } from "../structs/context/CommandContext.ts"; -import { extname, basename } from "node:path"; -import { safeSlice } from "../util.ts"; - -const GITHUB_LINE_URL_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:github)\.com\/(?[a-zA-Z0-9-_]+\/[A-Za-z0-9_.-]+)\/blob\/(?.+?)#L(?\d+)[-~]?L?(?\d*)/i; - -defineListener({ - event: Events.MessageCreate, - run: async(message: Message) => { - if (handleBunOnlyChannel(message)) return; - if (message.system || message.author.bot) return; - - if (!message.content.toLowerCase().startsWith(MESSAGE_PREFIX)) return handleOthers(message); - - const [commandName, ...args] = message.content - .slice(MESSAGE_PREFIX.length) - .trim() - .split(/ +/g); - - if (commandName.length === 0) return; - - const command = COMMANDS.get(commandName); - if (!command || !command.runMessage) return; - - const context = new MessageCommandContext(command, message, args); - - await Promise.resolve(command.runMessage(context)) - .catch(async error => { - context.reply({ - content: `Something went wrong... ${error.message} (${error.code})` - }); - }); - } -}); - -function handleOthers(message: Message) { - handleGithubLink(message); -} - -function handleBunOnlyChannel(message: Message) { - if (message.channel.id !== BUN_ONLY_CHANNEL_ID) return false; - - if (message.content !== "bun") { - message.delete(); - return true; - } - - message.react("🐰"); - return true; -} - -async function handleGithubLink(message: Message) { - const match = GITHUB_LINE_URL_REGEX.exec(message.content); - const groups = match?.groups; - if (!groups) return; - - const repo = groups.repo; - const path = groups.path; - let extension = extname(path).slice(1); - const firstLineNumber = parseInt(groups.first_line_number) - 1; - const secondLineNumber = parseInt(groups.second_line_number) || firstLineNumber + 1; - - const contentUrl = `https://raw.githubusercontent.com/${repo}/${path}`; - const response = await fetch(contentUrl); - const content = await response.text(); - const lines = content.split("\n"); - - // limit, max 25 lines - possible flood - if (secondLineNumber - firstLineNumber > 25 && lines.length > secondLineNumber) { - message.react("❌"); - return; - } - - let text = ""; - - for (let i = 0; i < lines.length; i++) { - if (i < firstLineNumber || i >= secondLineNumber) continue; - - const line = lines[i]; - text += `${line}\n`; - } - - // delete the last \n - text = text.slice(0, -1); - - if (extension === "zig") extension = "rs"; - - message.reply({ - content: `***${basename(path)}*** — *(L${firstLineNumber + 1}${secondLineNumber ? `-L${secondLineNumber}` : ""})*\n\`\`\`${extension}\n${safeSlice(text, 2000 - 6 - extension.length)}\n\`\`\``, - components: [ - new ActionRowBuilder() - .setComponents( - new ButtonBuilder() - .setLabel(repo) - .setStyle(ButtonStyle.Link) - .setURL(`https://github.com/${repo}/blob/${path}#L${firstLineNumber + 1}${secondLineNumber ? `-L${secondLineNumber}` : ""}`) - ) - .toJSON() - ] - }) -} diff --git a/src/listeners/message_create.tsx b/src/listeners/message_create.tsx new file mode 100644 index 0000000..c4350eb --- /dev/null +++ b/src/listeners/message_create.tsx @@ -0,0 +1,96 @@ +import { Message, ButtonStyle } from "lilybird"; +import { ActionRow, Button } from "@lilybird/jsx"; +import { extname, basename } from "node:path"; +import { Event } from "@lilybird/handlers"; +import { safeSlice } from "../util.ts"; + +const GITHUB_LINE_URL_REGEX = + /(?:https?:\/\/)?(?:www\.)?(?:github)\.com\/(?[a-zA-Z0-9-_]+\/[A-Za-z0-9_.-]+)\/blob\/(?.+?)#L(?\d+)[-~]?L?(?\d*)/i; + +export default { + event: "messageCreate", + run: async (message) => { + if (handleBunOnlyChannel(message)) return; + if (!message.content?.toLowerCase().startsWith(process.env.MESSAGE_PREFIX)) + return handleOthers(message); + }, +} satisfies Event<"messageCreate">; + +function handleOthers(message: Message): void { + handleGithubLink(message); +} + +function handleBunOnlyChannel(message: Message): boolean { + if (message.channelId !== process.env.BUN_ONLY_CHANNEL_ID) return false; + + if (message.content !== "bun") { + message.delete(); + return true; + } + + message.react("🐰"); + return true; +} + +async function handleGithubLink(message: Message): Promise { + if (!message.content) return; + + const match = GITHUB_LINE_URL_REGEX.exec(message.content); + const groups = match?.groups; + if (!groups) return; + + const repo = groups.repo; + const path = groups.path; + let extension = extname(path).slice(1); + const firstLineNumber = parseInt(groups.first_line_number) - 1; + const secondLineNumber = + parseInt(groups.second_line_number) || firstLineNumber + 1; + + const contentUrl = `https://raw.githubusercontent.com/${repo}/${path}`; + const response = await fetch(contentUrl); + const content = await response.text(); + const lines = content.split("\n"); + + // limit, max 25 lines - possible flood + if ( + secondLineNumber - firstLineNumber > 25 && + lines.length > secondLineNumber + ) { + message.react("❌"); + return; + } + + let text = ""; + + for (let i = 0; i < lines.length; i++) { + if (i < firstLineNumber || i >= secondLineNumber) continue; + + const line = lines[i]; + text += `${line}\n`; + } + + // delete the last \n + text = text.slice(0, -1); + + if (extension === "zig") extension = "rs"; + + message.reply({ + content: `***${basename(path)}*** — *(L${firstLineNumber + 1}${ + secondLineNumber ? `-L${secondLineNumber}` : "" + })*\n\`\`\`${extension}\n${safeSlice( + text, + 2000 - 6 - extension.length + )}\n\`\`\``, + components: [ + +