diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5c808bb..0b76e69 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,34 +1,33 @@ name: Validate on: - pull_request_target: + pull_request_target: jobs: - test: - runs-on: ubuntu-latest + test: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: refs/pull/${{ github.event.number }}/merge + steps: + - uses: actions/checkout@v2 + with: + ref: refs/pull/${{ github.event.number }}/merge - - name: Setup Bun - uses: xhyrom/setup-bun@v0.1.3 - with: - bun-version: latest - github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest - - name: Get changed files - uses: Mineflash07/gh-action-get-changed-files@feature/support-pr-target-event - with: - token: ${{ secrets.GITHUB_TOKEN }} + - name: Get changed files + uses: Mineflash07/gh-action-get-changed-files@feature/support-pr-target-event + with: + token: ${{ secrets.GITHUB_TOKEN }} - - name: Add json - run: cp $HOME/files.json ./scripts/validateTags/ + - name: Update files.json file + run: cp $HOME/files.json ./scripts/validate_tags/ - - name: Validate tag - run: bun run validate - env: - github-token: ${{ secrets.GITHUB_TOKEN }} - commit-sha: ${{ github.event.pull_request.head.sha }} - pr-number: ${{ github.event.pull_request.number }} \ No newline at end of file + - name: Validate tags + run: bun run validate:tags + env: + github-token: ${{ secrets.GITHUB_TOKEN }} + commit-sha: ${{ github.event.pull_request.head.sha }} + pr-number: ${{ github.event.pull_request.number }} diff --git a/.gitignore b/.gitignore index d2c0e42..54d3912 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ # Edit at https://www.toptal.com/developers/gitignore?templates=node files/database.sqlite -files/config.toml -requests.rest ### Node ### # Logs diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index a8d8fcb..0000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "tabWidth": 4, - "singleQuote": true, - "trailingComma": "es5" -} diff --git a/bun.lockb b/bun.lockb index 5e84d28..e1065dd 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/data/github.toml b/data/github.toml new file mode 100644 index 0000000..86f4413 --- /dev/null +++ b/data/github.toml @@ -0,0 +1,4 @@ +repositories = [ + "oven-sh/bun", + "xHyroM/bun-discord-bot" +] diff --git a/data/tags/bug.md b/data/tags/bug.md new file mode 100644 index 0000000..d805c96 --- /dev/null +++ b/data/tags/bug.md @@ -0,0 +1,9 @@ +--- +question: Found a bug in bun? +keywords: + - "bug" + - "report" + - "bugs" +--- +- If you are unsure, first discuss your bug in <#887787428973281300> or <#995247410794217553> +- [Open an issue on GitHub]() and fill out the given template diff --git a/data/tags/bun.md b/data/tags/bun.md new file mode 100644 index 0000000..c6ec5fc --- /dev/null +++ b/data/tags/bun.md @@ -0,0 +1,10 @@ +--- +question: What is bun? +keywords: + - "bun" + - "bun.sh" +--- + +Bun is an all-in-one JavaScript runtime & toolkit designed for speed, complete with a bundler, test runner, and Node.js-compatible package manager. + +Read more [here]() diff --git a/data/tags/dev-channel.md b/data/tags/dev-channel.md new file mode 100644 index 0000000..bbf2629 --- /dev/null +++ b/data/tags/dev-channel.md @@ -0,0 +1,12 @@ +--- +question: This is not a support channel where can i ask for help? +keywords: + - "development" + - "dev-channel" + - "codebase" + - "internals" +category_ids: + - "994349439005237369" # BUN INTERNALS category +--- + +This channel is for bun development, if you need help, <#1006402902513946735>, <#887787428973281300> and <#1004133980272078938> are here for you <:peekbun:995823659786711082> diff --git a/data/tags/feature-request.md b/data/tags/feature-request.md new file mode 100644 index 0000000..641f9f5 --- /dev/null +++ b/data/tags/feature-request.md @@ -0,0 +1,9 @@ +--- +question: Any idea what to add to bun? +keywords: + - "feature-request" + - "feature" + - "request" +--- +- Please consider if this is a request for bun, not for other 3rd party packages +- [Open an issue on GitHub]() and fill out the given template diff --git a/data/tags/illegal-instruction.md b/data/tags/illegal-instruction.md new file mode 100644 index 0000000..83268df --- /dev/null +++ b/data/tags/illegal-instruction.md @@ -0,0 +1,16 @@ +--- +question: "How to solve illegal instruction (core dumped) error?" +keywords: + - "illegal-instruction" + - "core-dumped" + - "dumped" + - "core" + - "illegal" +--- + +Update to latest version using: +```sh +curl https://bun.sh/install | bash +``` + +or install latest *-baseline build from [GitHub Releases]() diff --git a/data/tags/io-uring-is-not-supported.md b/data/tags/io-uring-is-not-supported.md new file mode 100644 index 0000000..c2c32cc --- /dev/null +++ b/data/tags/io-uring-is-not-supported.md @@ -0,0 +1,23 @@ +--- +question: How to upgrade linux kernel? +keywords: + - "io-uring-is-not-supported" + - "io uring" + - "kernel" + - "update" + - "linux" +--- + +To fix `io uring is not supported`, you need to update Linux kernel. +If you are using the Windows Subsystem for Linux, do: + **1.** Open powershell as administrator + **2.** Run: + - wsl --set-version 2 + - If it throws `Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS.`, turn on Virtual Machine Platform in Windows Features and then rerun this command. + - PS: You can get distro name from `wsl --list -v` + - wsl --update + - wsl --shutdown + +If that doesn't work (and you're on a Windows machine), try this: + **1.** Open Windows Update + **2.** Download any updates to Windows Subsystem for Linux diff --git a/data/tags/nix.md b/data/tags/nix.md new file mode 100644 index 0000000..40b4f44 --- /dev/null +++ b/data/tags/nix.md @@ -0,0 +1,14 @@ +--- +question: "How to use bun on NixOS?" +keywords: + - "nix" + - "nixos" +--- + +To use Bun on NixOS, you must install it through the nix package manager using [the `bun` package in nixpkgs](): + +```sh +nix-env -iA nixos.bun +``` + +Using `bun upgrade` or the curl installer will not work because NixOS does not provide `ld.so` which means any non-nixos linux executables will not work (you'll see "File not found" when trying to run them, even though the binary is there). diff --git a/data/tags/tags.md b/data/tags/tags.md new file mode 100644 index 0000000..d0b4352 --- /dev/null +++ b/data/tags/tags.md @@ -0,0 +1,8 @@ +--- +question: "Where can i add new tag to the bot?" +keywords: + - "tags" + - "contributing" +--- + +To create or update tag, check [xHyroM/bun-discord-bot#contributing-tags]() diff --git a/data/tags/ts.md b/data/tags/ts.md new file mode 100644 index 0000000..ee0f66c --- /dev/null +++ b/data/tags/ts.md @@ -0,0 +1,10 @@ +--- +question: "Do i need compiler for typescript?" +keywords: + - "ts" + - "typescript" + - "tsc" +--- + +Bun supports TypeScript and JSX out of the box. They can be run directly with [`bun run`](). +For setting up IDE support, [documentation page on TypeScript]() diff --git a/data/tags/windows.md b/data/tags/windows.md new file mode 100644 index 0000000..6648563 --- /dev/null +++ b/data/tags/windows.md @@ -0,0 +1,9 @@ +--- +question: "Where can i found windows version of bun?" +keywords: + - "windows" + - "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]() diff --git a/files/config.example.toml b/files/config.example.toml deleted file mode 100644 index 0b74b28..0000000 --- a/files/config.example.toml +++ /dev/null @@ -1,16 +0,0 @@ -# For start application rename it to config.toml !! - -[server] -port = 3000 - -[client] -public_key = "CLIENT PUBLIC KEY" -token = "CLIENT TOKEN" -id = "CLIENT USER ID" - -[commands] -guild_id = "GUILD ID" - -[api] -github_personal_access_token = "" -github_webhooks_secret = "" \ No newline at end of file diff --git a/files/tags.toml b/files/tags.toml deleted file mode 100644 index 5f52d2c..0000000 --- a/files/tags.toml +++ /dev/null @@ -1,90 +0,0 @@ -[bun] -keywords = ["bun", "bun.sh"] -content = """ -Bun is an all-in-one JavaScript runtime & toolkit designed for speed, complete with a bundler, test runner, and Node.js-compatible package manager. - -Read more [here]() -""" - -[bug] -keywords = ["bug", "report", "bugs"] -content = """ -Found a bug in bun? -- If you are unsure, first discuss your bug in <#887787428973281300> or <#995247410794217553> -- [Open an issue on GitHub]() and fill out the given template -""" - -[feature-request] -keywords = ["feature-request", "feature", "request"] -content = """ -Any idea what to add to bun? -- Please consider if this is a request for bun, not for other 3rd party packages -- [Open an issue on GitHub]() and fill out the given template -""" - -[tags] -keywords = ["tags", "contributing"] -content = """ -To create or update tag, check [xHyroM/bun-discord-bot#contributing-tags]() -""" - -[windows] -keywords = ["windows", "windows support"] -content = """ -Bun does not currently have support for Windows, so you must use WSL (Windows Subsystem for Linux). -To install WSL, check [microsoft documentation]() -""" - -[io-uring-is-not-supported] -keywords = ["io-uring-is-not-supported", "io uring", "not supported", "support", "linux", "kernel", "update"] -content = """ -**error: Linux kernel version doesn't support io_uring, which Bun depends on** - -To fix this error, you need to update Linux kernel. -If you are using the Windows Subsystem for Linux, do: - **1.** Open powershell as administrator - **2.** Run: - - wsl --set-version 2 - - If it throws `Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS.`, turn on Virtual Machine Platform in Windows Features and then rerun this command. - - PS: You can get distro name from `wsl --list -v` - - wsl --update - - wsl --shutdown - -If that doesn't work (and you're on a Windows machine), try this: - **1.** Open Windows Update - **2.** Download any updates to Windows Subsystem for Linux -""" - -[illegal-instruction] -keywords = ["illegal-instruction", "core-dumped", "dumped", "core", "illegal"] -content = """ -**Illegal instruction - core dumped** - -Update to latest version using: -+++ -curl https://bun.sh/install | bash -+++ - -or install latest *-baseline build from [GitHub Releases]() -""" - -# common questions - -[ts] # 'how do i use typescript or jsx?' -keywords = ["ts", "typescript", "tsc"] -content = """ -Bun supports TypeScript and JSX out of the box. They can be run directly with [`bun run`](). -For setting up IDE support, [documentation page on TypeScript]() -""" - -[nix] # 'it doesnt work on nixos' -keywords = ["nix", "nixos"] -content = """ -To use Bun on NixOS, you must install it through the nix package manager using [the `bun` package in nixpkgs](): - -+++ -nix-env -iA nixos.bun -+++ - -Using `bun upgrade` or the curl installer will not work because NixOS does not provide `ld.so` which means any non-nixos linux executables will not work (you'll see "File not found" when trying to run them, even though the binary is there). -""" diff --git a/files/utilities.toml b/files/utilities.toml deleted file mode 100644 index 679cb2a..0000000 --- a/files/utilities.toml +++ /dev/null @@ -1,5 +0,0 @@ -[github] -repositories = [ - "oven-sh/bun", - "xHyroM/bun-discord-bot" -] diff --git a/package.json b/package.json index f8456b8..852d268 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,17 @@ "name": "bun-discord-bot", "scripts": { "start": "bun src/index.ts", - "validate": "cd scripts/validateTags && bun install && bun start", - "cloudflare:tunnel": "sudo cloudflared tunnel run" + "validate:tags": "cd scripts/validate_tags && bun install && bun start" }, "devDependencies": { - "bun-types": "^0.1.8" + "bun-types": "^0.7.3" }, "dependencies": { - "@discordjs/collection": "^0.7.0", - "bun-utilities": "^0.2.1", - "create-hmac": "^1.1.7", - "discord-api-types": "^0.36.1", - "hono": "^1.6.4", - "minisearch": "^5.0.0", - "tweetnacl": "^1.0.3" + "@paperdave/logger": "^3.0.1", + "algoliasearch": "^4.19.1", + "discord.js": "^14.13.0", + "glob": "^10.3.3", + "gray-matter": "^4.0.3", + "zlib-sync": "^0.1.8" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..9e5b53e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,672 @@ +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/validateTags/files.json b/scripts/validateTags/files.json deleted file mode 100644 index a3a7bdd..0000000 --- a/scripts/validateTags/files.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "files/tags.toml" -] \ No newline at end of file diff --git a/scripts/validateTags/package.json b/scripts/validateTags/package.json deleted file mode 100644 index 5d0430d..0000000 --- a/scripts/validateTags/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "validate-tags", - "scripts": { - "start": "bun src/index.ts" - } -} \ No newline at end of file diff --git a/scripts/validateTags/src/index.ts b/scripts/validateTags/src/index.ts deleted file mode 100644 index c7805fb..0000000 --- a/scripts/validateTags/src/index.ts +++ /dev/null @@ -1,115 +0,0 @@ -interface Tag { - keywords: string[]; - content: string; -} - -const githubToken = process.env['github-token']; -const commitSha = process.env['commit-sha']; -const pullRequestNumber = process.env['pr-number']; - -const codeBlockRegex = /(`{1,3}).+?\1/gs; -const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/gi; -const files = await Bun.file('./files.json').text(); -if (!files.includes('files/tags.toml')) process.exit(0); - -const errors = []; - -let tags; -try { - // @ts-expect-error types - tags = (await import('../../../files/tags.toml')).default; - - requestGithub( - `issues/${pullRequestNumber}/labels`, - { - labels: ['tags'] - } - ); -} catch(e) { - tags = []; - errors.push(e.message); -} - -for (const [key, value] of Object.entries(tags)) { - const tag = value as Tag; - - 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.content) errors.push(`**[${key}]:** Tag must have content`); - - if (tag.content) { - const cleanedContent = tag.content.replaceAll('+++', '```').replace(codeBlockRegex, ''); - for (const url of cleanedContent.match(urlRegex) || []) { - const firstChar = tag.content.split(url)[0].slice(-1); - const lastChar = tag.content.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) { - requestGithub( - `pulls/${pullRequestNumber}/reviews`, - { - commit_id: commitSha, - event: 'APPROVE', - } - ); - - requestGithub( - `pulls/${pullRequestNumber}/requested_reviewers`, - { - reviewers: ['xHyroM'] - } - ); - - requestGithub( - `issues/${pullRequestNumber}/labels/waiting`, - {}, - 'DELETE' - ); - - requestGithub( - `issues/${pullRequestNumber}/labels`, - { - labels: ['ready'] - } - ); -} else { - requestGithub( - `pulls/${pullRequestNumber}/reviews`, - { - commit_id: commitSha, - body: '### Please fix the following problems:\n' + errors.join('\n'), - event: 'REQUEST_CHANGES', - } - ); - - requestGithub( - `issues/${pullRequestNumber}/labels`, - { - labels: ['waiting'] - } - ); -} - -function requestGithub(url: string, body: any, method?: 'POST' | 'DELETE') { - 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/validateTags/tsconfig.json b/scripts/validateTags/tsconfig.json deleted file mode 100644 index 6e28d70..0000000 --- a/scripts/validateTags/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "lib": ["ESNext"], - "module": "esnext", - "target": "esnext", - "moduleResolution": "Node", - // "bun-types" is the important part - "types": ["bun-types"] - } -} \ No newline at end of file diff --git a/scripts/validate_tags/bun.lockb b/scripts/validate_tags/bun.lockb new file mode 100755 index 0000000..69e6960 Binary files /dev/null and b/scripts/validate_tags/bun.lockb differ diff --git a/scripts/validate_tags/package.json b/scripts/validate_tags/package.json new file mode 100644 index 0000000..e7cd61a --- /dev/null +++ b/scripts/validate_tags/package.json @@ -0,0 +1,9 @@ +{ + "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 new file mode 100644 index 0000000..e72db3e --- /dev/null +++ b/scripts/validate_tags/src/index.ts @@ -0,0 +1,117 @@ +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/src/types.d.ts b/scripts/validate_tags/src/types.d.ts new file mode 100644 index 0000000..87dc72a --- /dev/null +++ b/scripts/validate_tags/src/types.d.ts @@ -0,0 +1,4 @@ +declare module '*.toml' { + const value: any; + export default value; +} diff --git a/scripts/validate_tags/tsconfig.json b/scripts/validate_tags/tsconfig.json new file mode 100644 index 0000000..4082f16 --- /dev/null +++ b/scripts/validate_tags/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/src/commands/docs.ts b/src/commands/docs.ts new file mode 100644 index 0000000..c9816b1 --- /dev/null +++ b/src/commands/docs.ts @@ -0,0 +1,94 @@ +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/github.ts b/src/commands/github.ts index c833bac..7f60ef3 100644 --- a/src/commands/github.ts +++ b/src/commands/github.ts @@ -1,135 +1,279 @@ -import { APIApplicationCommandInteractionDataStringOption, ApplicationCommandOptionType, InteractionResponseType, MessageFlags } from 'discord-api-types/v10'; -import { Command } from '../structures/Command'; -// @ts-expect-error Types :( -import utilities from '../../files/utilities.toml'; -import { CommandContext } from '../structures/contexts/CommandContext'; -import { getIssueOrPR, search, formatStatus, formatEmojiStatus, IssueState, IssueType } from '../utils/githubUtils'; +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"; -const invalidIssue = (ctx: CommandContext, query: string) => { - return ctx.editResponse( - `\`❌\` Invalid issue or pull request \`${query}\`. You can check [github search syntax](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests)` - ); +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; + } } -new Command({ - name: 'github', - description: 'Query an issue, pull request or direct link to Github Issue or PR', - options: [ - { - name: 'query', - description: 'Issue numer/name, PR number/name or direct link to Github Issue or PR', - type: ApplicationCommandOptionType.String, - required: true, - run: async(ctx) => { - return ctx.respond( - await search( - ctx.value, - (ctx.options.find(o => o.name === 'repository'))?.value as string || 'oven-sh/bun', - (ctx.options.find(o => o.name === 'state')?.value as string || 'all') as IssueState, - (ctx.options.find(o => o.name === 'type')?.value as string || '(IS|PR)') as IssueType, - ) - ); - } - }, - { - name: 'state', - description: 'Issue or PR state', - type: ApplicationCommandOptionType.String, - required: false, - choices: [ - { - name: 'open', - value: 'open', - }, - { - name: 'closed', - value: 'closed', - }, - { - name: 'merged', - value: 'merged', - }, - { - name: 'all (default)', - value: 'all', - } - ] - }, - { - name: 'type', - description: 'Issues or PRs', - type: ApplicationCommandOptionType.String, - required: false, - choices: [ - { - name: 'Issues', - value: '(IS)', - }, - { - name: 'Pull Requests', - value: '(PR)', - }, - { - name: 'Both', - value: '(IS|PR)', - } - ] - }, - { - name: 'repository', - description: 'Project repository (default oven-sh/bun)', - type: ApplicationCommandOptionType.String, - required: false, - choices: [ - ...utilities.github.repositories.map(repository => new Object({ - name: repository.split('/')[1], - value: repository - })) - ] - }, - ], - run: async(ctx) => { - ctx.command.runEditResponse(ctx) - return ctx.respond({ - type: InteractionResponseType.DeferredChannelMessageWithSource - }) +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() + })))); + } }, - runEditResponse: async(ctx) => { - let query: string = (ctx.options[0] as APIApplicationCommandInteractionDataStringOption).value; - const repository: string = (ctx.options.find(o => o.name === 'repository') as APIApplicationCommandInteractionDataStringOption)?.value || 'oven-sh/bun'; - const state: IssueState = ((ctx.options.find(o => o.name === 'state') as APIApplicationCommandInteractionDataStringOption)?.value || 'all') as IssueState; - const type: IssueType = ((ctx.options.find(o => o.name === 'type') as APIApplicationCommandInteractionDataStringOption)?.value || '(IS|PR)') as IssueType; - - const repositorySplit = repository.split('/'); - const repositoryOwner = repositorySplit[0]; - const repositoryName = repositorySplit[1]; - - let issueOrPR = await getIssueOrPR(parseInt(query), repository, state, type); - if (!issueOrPR) { - const res = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(query)}${encodeURIComponent(' repo:oven-sh/bun')}`); - - const data: any = await res.json(); - if (data.message || data?.items?.length === 0) return invalidIssue(ctx, query); - - const item = data.items[0]; - issueOrPR = { - id: item.number, - repository: item.repository_url.replace('https://api.github.com/repos/', ''), - title: item.title, - number: item.number, - state: item.state, - created_at: item.created_at, - closed_at: item.closed_at, - html_url: item.html_url, - user_login: item.user.login, - user_html_url: item.user.html_url, - type: item.pull_request ? '(PR)' : '(IS)', - }; - } - - return ctx.editResponse([ - `${formatEmojiStatus(issueOrPR)} [#${issueOrPR.number} ${issueOrPR.title.replace(/\[\]/g, '').slice(0, 1500)} (${repositoryOwner}/${repositoryName})](<${issueOrPR.html_url}>) by [${issueOrPR.user_login}](<${issueOrPR.user_html_url}>) ${formatStatus(issueOrPR)}`, - issueOrPR.title - ].join('\n')); + { + ...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) } -}) \ No newline at end of file + ], + 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/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..aa6cad5 --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,8 @@ +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 index 5bc9178..39e4683 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -1,16 +1,30 @@ -import { InteractionResponseType, MessageFlags } from 'discord-api-types/v10'; -import { Command } from '../structures/Command'; +import { defineCommand } from "../loaders/commands.ts"; +import { Bubu } from "../structs/Client.ts"; +import { InteractionCommandContext, MessageCommandContext } from "../structs/context/CommandContext.ts"; -new Command({ - name: 'ping', - description: 'pong', - run: (ctx) => { - return ctx.respond({ - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: 'Pong 🏓', - flags: MessageFlags.Ephemeral, - } - }) - } -}) \ No newline at end of file +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/tag.ts b/src/commands/tag.ts index 32e9a6d..a9a3ae0 100644 --- a/src/commands/tag.ts +++ b/src/commands/tag.ts @@ -1,54 +1,89 @@ -import { - APIApplicationCommandInteractionDataStringOption, - ApplicationCommandOptionType, - InteractionResponseType, - MessageFlags, -} from 'discord-api-types/v10'; -import { Command } from '../structures/Command'; -import { findTags, getTag } from '../utils/tagsUtils'; +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"; -new Command({ - name: 'tag', - description: 'Send a tag by name or alias', - options: [ - { - name: 'query', - description: 'Tag name or alias', - type: ApplicationCommandOptionType.String, - required: true, - run: (ctx) => { - return ctx.respond(findTags(ctx.value)); - }, - }, - { - name: 'target', - description: 'User to mention', - type: ApplicationCommandOptionType.User, - required: false, - }, - ], - run: (ctx) => { - const query: APIApplicationCommandInteractionDataStringOption = ctx - .options[0] as APIApplicationCommandInteractionDataStringOption; - const target = ctx?.resolved?.users - ? Object.values(ctx?.resolved?.users)[0] - : null; +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 tag = getTag(query.value, false); - if (!tag) - return ctx.respond({ - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: `\`❌\` Could not find a tag \`${query.value}\``, - flags: MessageFlags.Ephemeral, - }, - }); + const tags = searchTag(context.channel, query, true); + if (tags.length > 0) + return context.respond(tags); - return ctx.respond( - [ - target ? `*Tag suggestion for <@${target.id}>:*` : '', - tag.content, - ].join('\n') - ); + 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/version.ts b/src/commands/version.ts index 5cc58ba..f0f3723 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -1,22 +1,18 @@ -import { InteractionResponseType, MessageFlags } from 'discord-api-types/v10'; -import { Command } from '../structures/Command'; -import { exec } from 'bun-utilities/spawn'; +import { defineCommand } from "../loaders/commands.ts"; +import { COMMIT_HASH, PRODUCTION } from "../constants.ts"; +import { InteractionCommandContext } from "../structs/context/CommandContext.ts"; -const commitHash = exec(['git', 'log', '--pretty=format:\'%h\'', '-n', '1']).stdout.replaceAll('\'', ''); - -new Command({ - name: 'version', - description: 'Check bot and bun version', - run: (ctx) => { - return ctx.respond({ - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: [ - `Bot version: [git-bun-discord-bot-"${commitHash}"]()`, - `Bun version: [${process.version}]()`, - ].join('\n'), - flags: MessageFlags.Ephemeral, - } - }) - } -}) \ No newline at end of file +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/constants.ts b/src/constants.ts new file mode 100644 index 0000000..53c9f63 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,11 @@ +import { spawnSync } from "bun"; + +export const COMMIT_HASH = spawnSync({ + cmd: [ "git", "log", "--pretty=format:%h", "-n", "1" ] +}).stdout.toString(); + +export const PRODUCTION = process.env.NODE_ENV === "production"; + +export const MESSAGE_PREFIX = PRODUCTION ? "b" : "<>"; + + diff --git a/src/index.ts b/src/index.ts index 778423a..e6c0924 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,222 +1,11 @@ -import { Hono } from 'hono'; -import { bodyParse } from 'hono/body-parse'; -import { Logger } from './utils/Logger'; +import "./loaders/tags.ts"; +import "./commands"; +import "./listeners"; -// @ts-expect-error Types :( -import config from '../files/config.toml'; -import loadCommands from './utils/loadCommands'; -import { verifyGithubKey, verifyKey } from './utils/verify'; -import { - APIPingInteraction, - APIApplicationCommandInteraction, - APIMessageComponentInteraction, - InteractionType, - InteractionResponseType, - ApplicationCommandType, - APIApplicationCommandAutocompleteInteraction, - ApplicationCommandOptionType, -} from 'discord-api-types/v10'; -import { CommandContext } from './structures/contexts/CommandContext'; -import { Commands } from './managers/CommandManager'; -import registerCommands from './utils/registerCommands'; -import { Option, OptionOptions } from './structures/Option'; -import { AutocompleteContext } from './structures/contexts/AutocompleteContext'; -import { - deleteIssueOrPR, - fetchIssues, - fetchPullRequests, - setIssue, - setPullRequest, -} from './utils/githubUtils'; -import { removeExclamationFromNicknames } from './utils/discord'; +import { Bubu } from "./structs/Client.ts"; -await fetchIssues(); -await fetchPullRequests(); -(async () => { - Logger.info('Removing exclamation marks from nicknames...'); - await removeExclamationFromNicknames(config.client.token); - Logger.info('Removing is done!'); -})(); -await loadCommands(); -try { - await registerCommands(config.client.token, config.client.id); -} catch (e) { - console.log(e); -} +// Make sure bubu will not crash +process.on("unhandledRejection", console.error); +process.on("uncaughtException", console.error); -const app = new Hono(); -app.get('*', (c) => c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME')); // fireship :D - -app.post('/interaction', bodyParse(), async (c) => { - const signature = c.req.headers.get('X-Signature-Ed25519'); - const timestamp = c.req.headers.get('X-Signature-Timestamp'); - if (!signature || !timestamp) - return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D - if ( - !(await verifyKey( - JSON.stringify(c.req.parsedBody), - signature, - timestamp, - config.client.public_key - )) - ) - return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D - - const interaction = c.req.parsedBody as unknown as - | APIPingInteraction - | APIApplicationCommandInteraction - | APIMessageComponentInteraction - | APIApplicationCommandAutocompleteInteraction; - - if (interaction.type === InteractionType.Ping) { - return new CommandContext(c).respond({ - type: InteractionResponseType.Pong, - }); - } - - if ( - interaction.type === InteractionType.ApplicationCommandAutocomplete && - interaction.data.type === ApplicationCommandType.ChatInput - ) { - const command = Commands.get(interaction.data.name); - let options = command.options; - const subCommandGroup = interaction.data.options.find( - (option) => - option.type === ApplicationCommandOptionType.SubcommandGroup - ); - const subCommand = interaction.data.options.find( - (option) => option.type === ApplicationCommandOptionType.Subcommand - ); - - // @ts-expect-error ?? find - if (subCommandGroup) - options = options.find( - (option) => option.name === subCommandGroup.name - )?.options; - // @ts-expect-error ?? find - if (subCommand) - options = options.find( - (option) => option.name === subCommand.name - )?.options; - - // @ts-expect-error i dont want waste time - const focused: APIApplicationCommandBasicOption = - interaction.data.options.find((option) => option.focused === true); - // @ts-expect-error ?? find - const option: Option | OptionOptions = options.find( - (option) => option.name === focused.name - ); - - return option.run( - new AutocompleteContext( - c, - option, - focused.value, - interaction.data.options as any - ) - ); - } - - if ( - interaction.type === InteractionType.ApplicationCommand && - interaction.data.type === ApplicationCommandType.ChatInput - ) { - const commands = Commands.get(interaction.data.name); - return await commands.run(new CommandContext(c, commands, interaction)); - } - - return new CommandContext(c).respond({ - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: 'Beep boop. Boop beep?', - }, - }); -}); - -app.post('/github_webhook', bodyParse(), (c) => { - if ( - !c.req.headers.get('User-Agent').startsWith('GitHub-Hookshot/') || - typeof c.req?.parsedBody !== 'object' - ) - return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D - - if ( - !verifyGithubKey( - JSON.stringify(c.req.parsedBody), - c.req.headers.get('X-Hub-Signature-256'), - config.api.github_webhooks_secret - ) - ) - return c.redirect('https://www.youtube.com/watch?v=FMhScnY0dME'); // fireship :D - - const issueOrPr = c.req.parsedBody; - if (issueOrPr.action !== 'deleted') { - if ('issue' in issueOrPr) { - setIssue({ - id: issueOrPr.issue.number, - repository: issueOrPr.issue.repository_url.replace( - 'https://api.github.com/repos/', - '' - ), - title: issueOrPr.issue.title, - number: issueOrPr.issue.number, - state: issueOrPr.issue.state, - created_at: issueOrPr.issue.created_at, - closed_at: issueOrPr.issue.closed_at, - html_url: issueOrPr.issue.html_url, - user_login: issueOrPr.issue.user.login, - user_html_url: issueOrPr.issue.user.html_url, - type: '(IS)', - }); - } else if ('pull_request' in issueOrPr) { - setPullRequest({ - id: issueOrPr.pull_request.number, - repository: issueOrPr.pull_request.html_url - .replace('https://github.com/', '') - .replace(`/pull/${issueOrPr.pull_request.number}`, ''), - title: issueOrPr.pull_request.title, - number: issueOrPr.pull_request.number, - state: issueOrPr.pull_request.state, - created_at: issueOrPr.pull_request.created_at, - closed_at: issueOrPr.pull_request.closed_at, - merged_at: issueOrPr.pull_request.merged_at, - html_url: issueOrPr.pull_request.html_url, - user_login: issueOrPr.pull_request.user.login, - user_html_url: issueOrPr.pull_request.user.html_url, - type: '(PR)', - draft: issueOrPr.pull_request.draft, - }); - } - } else { - if ('issue' in issueOrPr) - deleteIssueOrPR( - issueOrPr.issue.number, - issueOrPr.issue.repository_url.replace( - 'https://api.github.com/repos/', - '' - ) - ); - else if ('pull_request' in issueOrPr) - deleteIssueOrPR( - issueOrPr.pull_request.number, - issueOrPr.pull_request.html_url - .replace('https://github.com/', '') - .replace(`/pull/${issueOrPr.pull_request.number}`, '') - ); - } - - return c.json( - { - message: 'OK', - }, - 200 - ); -}); - -await Bun.serve({ - port: config.server.port, - fetch: app.fetch, -}); - -Logger.info('🚀 Server started at', config.server.port.toString()); -Logger.debug(`🌍 http://localhost:${config.server.port}`); +Bubu.login(process.env.DISCORD_BOT_TOKEN).catch(console.error); diff --git a/src/listeners/index.ts b/src/listeners/index.ts new file mode 100644 index 0000000..29e628b --- /dev/null +++ b/src/listeners/index.ts @@ -0,0 +1,4 @@ +import "./ready.ts"; +import "./interaction_create.ts"; +import "./message_create.ts"; +import "./nickname_moderation.ts"; diff --git a/src/listeners/interaction_create.ts b/src/listeners/interaction_create.ts new file mode 100644 index 0000000..54ba7a1 --- /dev/null +++ b/src/listeners/interaction_create.ts @@ -0,0 +1,59 @@ +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/message_create.ts b/src/listeners/message_create.ts new file mode 100644 index 0000000..b14e568 --- /dev/null +++ b/src/listeners/message_create.ts @@ -0,0 +1,90 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Events, Message } from "discord.js"; +import { defineListener } from "../loaders/listeners.ts"; +import { MESSAGE_PREFIX } from "../constants.ts"; +import { COMMANDS } from "../loaders/commands.ts"; +import { MessageCommandContext } from "../structs/context/CommandContext.ts"; +import { extname } 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 (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); +} + +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; + const 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); + + message.reply({ + content: `\`\`\`${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/nickname_moderation.ts b/src/listeners/nickname_moderation.ts new file mode 100644 index 0000000..e4e113e --- /dev/null +++ b/src/listeners/nickname_moderation.ts @@ -0,0 +1,25 @@ +import { Events, GuildMember } from "discord.js"; +import { defineListener } from "../loaders/listeners.ts"; +import { silently } from "../util.ts"; + +defineListener({ + event: Events.GuildMemberAdd, + run: (member: GuildMember) => moderateNick(member) +}); + +defineListener({ + event: Events.GuildMemberUpdate, + run: (_: GuildMember, newMember: GuildMember) => moderateNick(newMember) +}); + +async function moderateNick(member: GuildMember) { + const name = member.displayName; + const normalizedName = name.normalize("NFKC").replace(/^[!$#@%^`&*()]+/, ""); + + if (name === normalizedName) return; + + silently(member.edit({ + nick: normalizedName, + reason: "lame username" + })); +} diff --git a/src/listeners/ready.ts b/src/listeners/ready.ts new file mode 100644 index 0000000..006d388 --- /dev/null +++ b/src/listeners/ready.ts @@ -0,0 +1,11 @@ +import { info } from "@paperdave/logger"; +import { defineListener } from "../loaders/listeners.ts"; +import { Client, Events } from "discord.js"; + +defineListener({ + event: Events.ClientReady, + once: true, + run: (client: Client) => { + info(`Logged in as ${client.user.tag} (${client.user.id})`); + } +}) diff --git a/src/loaders/commands.ts b/src/loaders/commands.ts new file mode 100644 index 0000000..bb07fd6 --- /dev/null +++ b/src/loaders/commands.ts @@ -0,0 +1,26 @@ +import { REST, Routes, SlashCommandBuilder } from "discord.js"; +import type { Command } from "../structs/Command.ts"; + +export const COMMANDS: Map = new Map(); +export const REST_CLIENT = new REST().setToken(process.env.DISCORD_TOKEN); + +export function defineCommand(command: T) { + COMMANDS.set(command.name, command); +} + +export async function registerCommands() { + const commands = [...COMMANDS.values()]; + + await REST_CLIENT.put( + Routes.applicationCommands("995690041793839124"), + { + body: commands.map(d => ({ + ...new SlashCommandBuilder() + .setName(d.name) + .setDescription(d.description) + .toJSON(), + options: d.options ?? [] + })) + } + ) +} diff --git a/src/loaders/listeners.ts b/src/loaders/listeners.ts new file mode 100644 index 0000000..111e7fc --- /dev/null +++ b/src/loaders/listeners.ts @@ -0,0 +1,14 @@ +import type { ClientEvents } from "discord.js"; +import { Bubu } from "../structs/Client.ts"; +import { Listener } from "../structs/Listener.ts"; + +export const LISTENERS: Listener[] = []; + +export function defineListener>(listener: T) { + LISTENERS.push(listener); + + Bubu[listener.once ? "once" : "on"]( + listener.event as keyof ClientEvents, + listener.run.bind(this) + ); +} diff --git a/src/loaders/tags.ts b/src/loaders/tags.ts new file mode 100644 index 0000000..3bcb64b --- /dev/null +++ b/src/loaders/tags.ts @@ -0,0 +1,102 @@ +import matter from "gray-matter"; +import { readFileSync } from "node:fs"; +import { globSync as glob } from "glob"; +import { join } from "node:path"; +import { Tag } from "../structs/Tag"; +import { APIApplicationCommandOptionChoice, BaseGuildTextChannel, GuildTextBasedChannel, TextBasedChannel } from "discord.js"; +import { safeSlice } from "../util"; + +const tags = glob(join(__dirname, "..", "..", "data", "tags", "*.md")); + +export const TAGS: Tag[] = []; + +for (const tag of tags) { + const content = readFileSync(tag); + const frontMatter = matter(content); + + TAGS.push({ + 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 + }); +} + +function getAvailableTags(channel: GuildTextBasedChannel) { + return TAGS.filter(t => { + const channelIds = t.channel_ids; + const categoryIds = t.category_ids; + + if (!channelIds && !categoryIds) return true; + if (channelIds && channelIds.includes(channel.id)) return true; + if (categoryIds && categoryIds.includes(channel.parentId)) return true; + + return false; + }); + +} + +export function getTags(channel: GuildTextBasedChannel, length: number): APIApplicationCommandOptionChoice[] { + const availableTags = getAvailableTags(channel); + return safeSlice( + availableTags.map((tag) => ( + { + name: `🚀 ${tag.question}`, + value: tag.question + } + )), + length); +} + +export function searchTag(channel: GuildTextBasedChannel, providedQuery: string, multiple?: T): T extends true ? APIApplicationCommandOptionChoice[] : Tag { + const availableTags = getAvailableTags(channel); + const query = providedQuery?.toLowerCase()?.replace(/-/g, " "); + + if (!multiple) { + const exactKeyword = availableTags.find(tag => tag.keywords.find((k) => k.toLowerCase() === query)); + const keywordMatch = availableTags.find(tag => tag.keywords.find((k) => k.toLowerCase().includes(query))); + const questionMatch = availableTags.find(tag => tag.question.toLowerCase().includes(query)); + const answerMatch = availableTags.find(tag => tag.answer.toLowerCase().includes(query)); + + const tag = exactKeyword ?? questionMatch ?? keywordMatch ?? answerMatch; + return tag as T extends true ? APIApplicationCommandOptionChoice[] : Tag; + } + + const exactKeywords: APIApplicationCommandOptionChoice[] = []; + const keywordMatches: APIApplicationCommandOptionChoice[] = []; + const questionMatches: APIApplicationCommandOptionChoice[] = []; + const answerMatches: APIApplicationCommandOptionChoice[] = []; + + for (const tag of availableTags) { + const exactKeyword = tag.keywords.find((t) => t.toLowerCase() === query); + const includesKeyword = tag.keywords.find((t) => t.toLowerCase().includes(query)); + const questionMatch = tag.question.toLowerCase().includes(query); + const answerMatch = tag.answer.toLowerCase().includes(query); + + if (exactKeyword) { + exactKeywords.push({ + name: `✅ ${tag.question}`, + value: tag.question + }); + } else if (includesKeyword) { + keywordMatches.push({ + name: `🔑 ${tag.question}`, + value: tag.question, + }) + } else if(questionMatch) { + questionMatches.push({ + name: `❓ ${tag.question}`, + value: tag.question + }) + } else if (answerMatch) { + answerMatches.push({ + name: `📄 ${tag.question}`, + value: tag.question + }) + } + } + + const tags = [...exactKeywords, ...questionMatches, ...keywordMatches, ...answerMatches]; + return tags as T extends true ? APIApplicationCommandOptionChoice[] : Tag; +} diff --git a/src/managers/CommandManager.ts b/src/managers/CommandManager.ts deleted file mode 100644 index 8b29447..0000000 --- a/src/managers/CommandManager.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Collection from '@discordjs/collection'; -import { Command } from '../structures/Command'; - -class CommandManager extends Collection { - constructor() { - super(); - } - - public register(command: Command): CommandManager { - this.set(command.name, command); - return this; - } - - public unregister(command: Command): CommandManager { - this.delete(command.name); - return this; - } -} - -export const Commands = new CommandManager(); \ No newline at end of file diff --git a/src/structs/Client.ts b/src/structs/Client.ts new file mode 100644 index 0000000..27cba2a --- /dev/null +++ b/src/structs/Client.ts @@ -0,0 +1,11 @@ +import { Client, GatewayIntentBits } from "discord.js"; + +export const Bubu = new Client({ + intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent | GatewayIntentBits.GuildMembers, + allowedMentions: { + parse: [], + repliedUser: false, + } +}); + +Bubu.on("error", console.log); diff --git a/src/structs/Command.ts b/src/structs/Command.ts new file mode 100644 index 0000000..8f3d06e --- /dev/null +++ b/src/structs/Command.ts @@ -0,0 +1,41 @@ +import { APIApplicationCommandAttachmentOption, APIApplicationCommandBasicOption, APIApplicationCommandBooleanOption, APIApplicationCommandChannelOption, APIApplicationCommandIntegerOption, APIApplicationCommandMentionableOption, APIApplicationCommandNumberOption, APIApplicationCommandOptionChoice, APIApplicationCommandRoleOption, APIApplicationCommandStringOption, APIApplicationCommandSubcommandGroupOption, APIApplicationCommandSubcommandOption, APIApplicationCommandUserOption, ApplicationCommandOptionType, LocalizationMap, SharedNameAndDescription, SlashCommandAttachmentOption, SlashCommandBooleanOption, SlashCommandChannelOption, SlashCommandIntegerOption, SlashCommandMentionableOption, SlashCommandNumberOption, SlashCommandRoleOption, SlashCommandStringOption, SlashCommandUserOption } from "discord.js"; +import { AutocompleteContext } from "./context/AutocompleteContext"; +import { CommandContext } from "./context/CommandContext"; + +export type Option = APIApplicationCommandAttachmentOption | + APIApplicationCommandBooleanOption | + APIApplicationCommandChannelOption | + APIApplicationCommandIntegerOption | + APIApplicationCommandMentionableOption | + APIApplicationCommandNumberOption | + APIApplicationCommandRoleOption | + APIApplicationCommandUserOption | + APIApplicationCommandSubcommandOption | + APIApplicationCommandSubcommandGroupOption | + StringOption; + +export interface StringOption { + name: string; + name_localizations?: LocalizationMap; + description: string; + description_localizations?: LocalizationMap; + min_length?: number; + max_length?: number; + required?: boolean; + type: ApplicationCommandOptionType.String; + autocomplete?: boolean; + choices?: APIApplicationCommandOptionChoice[]; + run?: (interaction: AutocompleteContext) => any; +} + +export interface Command { + name: string; + description: string; + options?: Option[]; + run?: ( + context: CommandContext + ) => any; + runMessage?: ( + context: CommandContext + ) => any; +} diff --git a/src/structs/Listener.ts b/src/structs/Listener.ts new file mode 100644 index 0000000..84e8488 --- /dev/null +++ b/src/structs/Listener.ts @@ -0,0 +1,10 @@ +import type { ClientEvents } from "discord.js"; + +export interface Listener { + event: E; + once?: boolean; + run: ( + ...args: ClientEvents[E] + ) => any; +} + diff --git a/src/structs/Tag.ts b/src/structs/Tag.ts new file mode 100644 index 0000000..669c1d8 --- /dev/null +++ b/src/structs/Tag.ts @@ -0,0 +1,8 @@ +export interface Tag { + name: string; + question: string; + answer: string; + keywords: string[]; + category_ids: string[] | null; + channel_ids: string[] | null; +} diff --git a/src/structs/context/AutocompleteContext.ts b/src/structs/context/AutocompleteContext.ts new file mode 100644 index 0000000..7b114e9 --- /dev/null +++ b/src/structs/context/AutocompleteContext.ts @@ -0,0 +1,34 @@ +import { ApplicationCommandOptionChoiceData, AutocompleteInteraction } from "discord.js"; +import { Command, Option } from "../Command.ts"; + +export class AutocompleteContext { + public option: Option; + public command: Command; + public interaction: AutocompleteInteraction; + + public constructor(option: Option, command: Command, interaction: AutocompleteInteraction) { + this.option = option; + this.command = command; + this.interaction = interaction; + } + + get channel() { + return this.interaction.channel; + } + + get user() { + return this.interaction.user; + } + + get member() { + return this.interaction.member; + } + + get options() { + return this.interaction.options; + } + + public respond(options: ApplicationCommandOptionChoiceData[]) { + return this.interaction.respond(options); + } +} diff --git a/src/structs/context/CommandContext.ts b/src/structs/context/CommandContext.ts new file mode 100644 index 0000000..b1bfdbf --- /dev/null +++ b/src/structs/context/CommandContext.ts @@ -0,0 +1,72 @@ +import { ChatInputCommandInteraction, InteractionReplyOptions, Message, MessageCreateOptions, MessagePayload, InteractionResponse, Channel, User, GuildMember, APIInteractionGuildMember } from "discord.js"; +import type { Command } from "../Command.ts"; + +export interface CommandContext { + command: Command; + + user: User; + member: GuildMember | APIInteractionGuildMember; + channel: Channel; + + reply(options: string | MessagePayload | (T extends true ? InteractionReplyOptions : MessageCreateOptions)): (T extends true ? Promise : Promise); +} + +export class InteractionCommandContext implements CommandContext { + public command: Command; + public interaction: ChatInputCommandInteraction; + + public constructor(command: Command, interaction: ChatInputCommandInteraction) { + this.command = command; + this.interaction = interaction; + } + + get user() { + return this.interaction.user; + } + + get member() { + return this.interaction.member; + } + + get channel() { + return this.interaction.channel; + } + + get resolved() { + return this.interaction.options.resolved; + } + + public reply(options: string | MessagePayload | InteractionReplyOptions): Promise { + return this.interaction.reply(options); + } +} + +export class MessageCommandContext implements CommandContext { + public command: Command; + public message: Message; + public options: string[]; + + public constructor(command: Command, message: Message, args: string[]) { + this.command = command; + this.message = message; + + // TODO: change args structure to application commands like + this.options = args; + } + + get user() { + return this.message.author; + } + + get member() { + return this.message.member; + } + + get channel() { + return this.message.channel; + } + + public reply(options: string | MessagePayload | MessageCreateOptions): Promise> { + return this.channel.send(options); + } +} diff --git a/src/structures/Command.ts b/src/structures/Command.ts deleted file mode 100644 index 6dd2a2f..0000000 --- a/src/structures/Command.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Taken from https://github.com/Garlic-Team/gcommands/blob/next/src/lib/structures/Command.ts - -// @ts-expect-error Types :( -import config from '../../files/config.toml'; -import { LocaleString } from 'discord-api-types/v10'; -import { Commands } from '../managers/CommandManager'; -import { CommandContext } from './contexts/CommandContext'; -import { Option, OptionOptions } from './Option'; - -export interface CommandOptions { - name: string; - nameLocalizations?: Record; - description?: string; - descriptionLocalizations?: Record; - guildId?: string; - defaultMemberPermissions?: string; - options?: Option[] | OptionOptions[]; - run: (ctx: CommandContext) => Response | Promise; - runEditResponse?: (ctx: CommandContext) => any; -} - -export class Command { - public name: string; - public nameLocalizations?: Record; - public description?: string; - public descriptionLocalizations?: Record; - public guildId?: string = config.client.guild_id; - public defaultMemberPermissions?: string; - public options: Option[] | OptionOptions[]; - public run: (ctx: CommandContext) => Response | Promise; - public runEditResponse: (ctx: CommandContext) => any; - - public constructor(options: CommandOptions) { - this.name = options.name; - this.nameLocalizations = options.nameLocalizations; - - this.description = options.description; - this.descriptionLocalizations = options.descriptionLocalizations; - - this.guildId = options.guildId; - this.defaultMemberPermissions = options.defaultMemberPermissions; - - this.options = options.options?.map(option => { - if (option instanceof Option) return option; - else return new Option(option); - }); - this.run = options.run; - this.runEditResponse = options.runEditResponse; - - Commands.register(this); - } - - public toJSON(): Record { - return { - name: this.name, - name_localizations: this.nameLocalizations, - description: this.description, - description_localizations: this.descriptionLocalizations, - guild_id: this.guildId, - default_member_permissions: this.defaultMemberPermissions, - options: this.options?.map(option => option.toJSON()), - } - } -} \ No newline at end of file diff --git a/src/structures/Option.ts b/src/structures/Option.ts deleted file mode 100644 index e4683a9..0000000 --- a/src/structures/Option.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Taken from https://github.com/Garlic-Team/gcommands/blob/next/src/lib/structures/Argument.ts - -import { ApplicationCommandOptionType, ChannelType, LocaleString } from 'discord-api-types/v10'; -import { AutocompleteContext } from './contexts/AutocompleteContext'; - -export interface OptionChoice { - name: string; - nameLocalizations?: Record; - value: string | number; -} - -export interface OptionOptions { - name: string; - nameLocalizations?: Record; - description: string; - descriptionLocalizations?: Record; - type: ApplicationCommandOptionType - required?: boolean; - choices?: OptionChoice[]; - options?: Array