mirror of
https://github.com/xHyroM/bun-discord-bot.git
synced 2024-11-10 01:08:07 +01:00
commit
200dd5a665
66 changed files with 2015 additions and 1605 deletions
47
.github/workflows/validate.yml
vendored
47
.github/workflows/validate.yml
vendored
|
@ -1,34 +1,33 @@
|
||||||
name: Validate
|
name: Validate
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: refs/pull/${{ github.event.number }}/merge
|
ref: refs/pull/${{ github.event.number }}/merge
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: xhyrom/setup-bun@v0.1.3
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
uses: Mineflash07/gh-action-get-changed-files@feature/support-pr-target-event
|
uses: Mineflash07/gh-action-get-changed-files@feature/support-pr-target-event
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Add json
|
- name: Update files.json file
|
||||||
run: cp $HOME/files.json ./scripts/validateTags/
|
run: cp $HOME/files.json ./scripts/validate_tags/
|
||||||
|
|
||||||
- name: Validate tag
|
- name: Validate tags
|
||||||
run: bun run validate
|
run: bun run validate:tags
|
||||||
env:
|
env:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-sha: ${{ github.event.pull_request.head.sha }}
|
commit-sha: ${{ github.event.pull_request.head.sha }}
|
||||||
pr-number: ${{ github.event.pull_request.number }}
|
pr-number: ${{ github.event.pull_request.number }}
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,8 +2,6 @@
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||||
|
|
||||||
files/database.sqlite
|
files/database.sqlite
|
||||||
files/config.toml
|
|
||||||
requests.rest
|
|
||||||
|
|
||||||
### Node ###
|
### Node ###
|
||||||
# Logs
|
# Logs
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"tabWidth": 4,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
4
data/github.toml
Normal file
4
data/github.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
repositories = [
|
||||||
|
"oven-sh/bun",
|
||||||
|
"xHyroM/bun-discord-bot"
|
||||||
|
]
|
9
data/tags/bug.md
Normal file
9
data/tags/bug.md
Normal file
|
@ -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](<https://github.com/oven-sh/bun/issues/new?template=1-bug-report.yml>) and fill out the given template
|
10
data/tags/bun.md
Normal file
10
data/tags/bun.md
Normal file
|
@ -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](<https://bun.sh/>)
|
12
data/tags/dev-channel.md
Normal file
12
data/tags/dev-channel.md
Normal file
|
@ -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>
|
9
data/tags/feature-request.md
Normal file
9
data/tags/feature-request.md
Normal file
|
@ -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](<https://github.com/oven-sh/bun/issues/new?template=2-feature-request.yml>) and fill out the given template
|
16
data/tags/illegal-instruction.md
Normal file
16
data/tags/illegal-instruction.md
Normal file
|
@ -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](<https://github.com/oven-sh/bun/releases>)
|
23
data/tags/io-uring-is-not-supported.md
Normal file
23
data/tags/io-uring-is-not-supported.md
Normal file
|
@ -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 <disto name> 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
|
14
data/tags/nix.md
Normal file
14
data/tags/nix.md
Normal file
|
@ -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](<https://search.nixos.org/packages?channel=unstable&show=bun&from=0&size=1&sort=relevance&type=packages&query=bun>):
|
||||||
|
|
||||||
|
```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).
|
8
data/tags/tags.md
Normal file
8
data/tags/tags.md
Normal file
|
@ -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](<https://github.com/xHyroM/bun-discord-bot/#contributing-tags>)
|
10
data/tags/ts.md
Normal file
10
data/tags/ts.md
Normal file
|
@ -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`](<https://bun.sh/docs/cli/run>).
|
||||||
|
For setting up IDE support, [documentation page on TypeScript](<https://bun.sh/docs/runtime/typescript>)
|
9
data/tags/windows.md
Normal file
9
data/tags/windows.md
Normal file
|
@ -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](<https://docs.microsoft.com/en-us/windows/wsl/install>)
|
|
@ -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 = ""
|
|
|
@ -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](<https://bun.sh/>)
|
|
||||||
"""
|
|
||||||
|
|
||||||
[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](<https://github.com/oven-sh/bun/issues/new?template=1-bug-report.yml>) 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](<https://github.com/oven-sh/bun/issues/new?template=2-feature-request.yml>) and fill out the given template
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tags]
|
|
||||||
keywords = ["tags", "contributing"]
|
|
||||||
content = """
|
|
||||||
To create or update tag, check [xHyroM/bun-discord-bot#contributing-tags](<https://github.com/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](<https://docs.microsoft.com/en-us/windows/wsl/install>)
|
|
||||||
"""
|
|
||||||
|
|
||||||
[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 <disto name> 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](<https://github.com/oven-sh/bun/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`](<https://bun.sh/docs/cli/run>).
|
|
||||||
For setting up IDE support, [documentation page on TypeScript](<https://bun.sh/docs/runtime/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](<https://search.nixos.org/packages?channel=unstable&show=bun&from=0&size=1&sort=relevance&type=packages&query=bun>):
|
|
||||||
|
|
||||||
+++
|
|
||||||
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).
|
|
||||||
"""
|
|
|
@ -1,5 +0,0 @@
|
||||||
[github]
|
|
||||||
repositories = [
|
|
||||||
"oven-sh/bun",
|
|
||||||
"xHyroM/bun-discord-bot"
|
|
||||||
]
|
|
18
package.json
18
package.json
|
@ -3,19 +3,17 @@
|
||||||
"name": "bun-discord-bot",
|
"name": "bun-discord-bot",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun src/index.ts",
|
"start": "bun src/index.ts",
|
||||||
"validate": "cd scripts/validateTags && bun install && bun start",
|
"validate:tags": "cd scripts/validate_tags && bun install && bun start"
|
||||||
"cloudflare:tunnel": "sudo cloudflared tunnel run"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "^0.1.8"
|
"bun-types": "^0.7.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/collection": "^0.7.0",
|
"@paperdave/logger": "^3.0.1",
|
||||||
"bun-utilities": "^0.2.1",
|
"algoliasearch": "^4.19.1",
|
||||||
"create-hmac": "^1.1.7",
|
"discord.js": "^14.13.0",
|
||||||
"discord-api-types": "^0.36.1",
|
"glob": "^10.3.3",
|
||||||
"hono": "^1.6.4",
|
"gray-matter": "^4.0.3",
|
||||||
"minisearch": "^5.0.0",
|
"zlib-sync": "^0.1.8"
|
||||||
"tweetnacl": "^1.0.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
672
pnpm-lock.yaml
Normal file
672
pnpm-lock.yaml
Normal file
|
@ -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
|
|
@ -1,3 +0,0 @@
|
||||||
[
|
|
||||||
"files/tags.toml"
|
|
||||||
]
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "validate-tags",
|
|
||||||
"scripts": {
|
|
||||||
"start": "bun src/index.ts"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 { };
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"module": "esnext",
|
|
||||||
"target": "esnext",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
// "bun-types" is the important part
|
|
||||||
"types": ["bun-types"]
|
|
||||||
}
|
|
||||||
}
|
|
BIN
scripts/validate_tags/bun.lockb
Executable file
BIN
scripts/validate_tags/bun.lockb
Executable file
Binary file not shown.
9
scripts/validate_tags/package.json
Normal file
9
scripts/validate_tags/package.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "validate_tags",
|
||||||
|
"scripts": {
|
||||||
|
"start": "bun src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^10.3.3"
|
||||||
|
}
|
||||||
|
}
|
117
scripts/validate_tags/src/index.ts
Normal file
117
scripts/validate_tags/src/index.ts
Normal file
|
@ -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 {};
|
4
scripts/validate_tags/src/types.d.ts
vendored
Normal file
4
scripts/validate_tags/src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
declare module '*.toml' {
|
||||||
|
const value: any;
|
||||||
|
export default value;
|
||||||
|
}
|
3
scripts/validate_tags/tsconfig.json
Normal file
3
scripts/validate_tags/tsconfig.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json"
|
||||||
|
}
|
94
src/commands/docs.ts
Normal file
94
src/commands/docs.ts
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,135 +1,279 @@
|
||||||
import { APIApplicationCommandInteractionDataStringOption, ApplicationCommandOptionType, InteractionResponseType, MessageFlags } from 'discord-api-types/v10';
|
import { SlashCommandBooleanOption, SlashCommandStringOption, time } from "discord.js";
|
||||||
import { Command } from '../structures/Command';
|
import { defineCommand } from "../loaders/commands.ts";
|
||||||
// @ts-expect-error Types :(
|
import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts";
|
||||||
import utilities from '../../files/utilities.toml';
|
import { InteractionCommandContext } from "../structs/context/CommandContext.ts";
|
||||||
import { CommandContext } from '../structures/contexts/CommandContext';
|
import { safeSlice, silently } from "../util.ts";
|
||||||
import { getIssueOrPR, search, formatStatus, formatEmojiStatus, IssueState, IssueType } from '../utils/githubUtils';
|
|
||||||
|
|
||||||
const invalidIssue = (ctx: CommandContext, query: string) => {
|
type State = "open" | "closed_as_completed" | "closed_as_not_planned" | "closed" | "merged" | "draft" | "all";
|
||||||
return ctx.editResponse(
|
type StateEmoji = "🔴" | "🟠" | "🟢" | "⚫️" | "⚪️" | "🟣" | "📝";
|
||||||
`\`❌\` 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 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({
|
defineCommand({
|
||||||
name: 'github',
|
name: "github",
|
||||||
description: 'Query an issue, pull request or direct link to Github Issue or PR',
|
description: "Query an issue, pull request or direct link to issue, pull request",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'query',
|
...new SlashCommandStringOption()
|
||||||
description: 'Issue numer/name, PR number/name or direct link to Github Issue or PR',
|
.setName("query")
|
||||||
type: ApplicationCommandOptionType.String,
|
.setDescription("Issue/Pull request number or name")
|
||||||
required: true,
|
.setRequired(true)
|
||||||
run: async(ctx) => {
|
.setAutocomplete(true)
|
||||||
return ctx.respond(
|
.setMaxLength(100),
|
||||||
await search(
|
run: async(ctx: AutocompleteContext) => {
|
||||||
ctx.value,
|
const query = ctx.options.getString("query");
|
||||||
(ctx.options.find(o => o.name === 'repository'))?.value as string || 'oven-sh/bun',
|
const state: State = ctx.options.getString("state") as State || "all";
|
||||||
(ctx.options.find(o => o.name === 'state')?.value as string || 'all') as IssueState,
|
const type: Type = ctx.options.getString("type") as Type || "both";
|
||||||
(ctx.options.find(o => o.name === 'type')?.value as string || '(IS|PR)') as IssueType,
|
|
||||||
)
|
const response = await search(query, state, type, 25);
|
||||||
);
|
|
||||||
}
|
await silently(ctx.respond(response.map(r => ({
|
||||||
},
|
name: safeSlice<string>(`${r.emoji.type} ${r.emoji.state} #${r.number} | ${r.title}`, 100),
|
||||||
{
|
value: r.number.toString()
|
||||||
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
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
runEditResponse: async(ctx) => {
|
{
|
||||||
let query: string = (ctx.options[0] as APIApplicationCommandInteractionDataStringOption).value;
|
...new SlashCommandStringOption()
|
||||||
const repository: string = (ctx.options.find(o => o.name === 'repository') as APIApplicationCommandInteractionDataStringOption)?.value || 'oven-sh/bun';
|
.setName("state")
|
||||||
const state: IssueState = ((ctx.options.find(o => o.name === 'state') as APIApplicationCommandInteractionDataStringOption)?.value || 'all') as IssueState;
|
.setDescription("Issue or Pull request state")
|
||||||
const type: IssueType = ((ctx.options.find(o => o.name === 'type') as APIApplicationCommandInteractionDataStringOption)?.value || '(IS|PR)') as IssueType;
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
const repositorySplit = repository.split('/');
|
{
|
||||||
const repositoryOwner = repositorySplit[0];
|
name: "🔴🟠 Open",
|
||||||
const repositoryName = repositorySplit[1];
|
value: "open"
|
||||||
|
},
|
||||||
let issueOrPR = await getIssueOrPR(parseInt(query), repository, state, type);
|
{
|
||||||
if (!issueOrPR) {
|
name: "🟢 Closed as completed",
|
||||||
const res = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(query)}${encodeURIComponent(' repo:oven-sh/bun')}`);
|
value: "closed_as_completed"
|
||||||
|
},
|
||||||
const data: any = await res.json();
|
{
|
||||||
if (data.message || data?.items?.length === 0) return invalidIssue(ctx, query);
|
name: "⚪️ Closed as not planned",
|
||||||
|
value: "closed_as_not_planned"
|
||||||
const item = data.items[0];
|
},
|
||||||
issueOrPR = {
|
{
|
||||||
id: item.number,
|
name: "⚫️ Closed",
|
||||||
repository: item.repository_url.replace('https://api.github.com/repos/', ''),
|
value: "closed"
|
||||||
title: item.title,
|
},
|
||||||
number: item.number,
|
{
|
||||||
state: item.state,
|
name: "🟣 Merged",
|
||||||
created_at: item.created_at,
|
value: "merged"
|
||||||
closed_at: item.closed_at,
|
},
|
||||||
html_url: item.html_url,
|
{
|
||||||
user_login: item.user.login,
|
name: "📝 Draft",
|
||||||
user_html_url: item.user.html_url,
|
value: "draft",
|
||||||
type: item.pull_request ? '(PR)' : '(IS)',
|
},
|
||||||
};
|
{
|
||||||
}
|
name: "🌍 All",
|
||||||
|
value: "all",
|
||||||
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("type")
|
||||||
|
.setDescription("Issue or Pull Requests")
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
{
|
||||||
|
name: "🐛 Issues",
|
||||||
|
value: "issues"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "🔨 Pull Requests",
|
||||||
|
value: "pull_requests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "🌍 Both",
|
||||||
|
value: "both"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...new SlashCommandBooleanOption()
|
||||||
|
.setName("hide")
|
||||||
|
.setDescription("Show this message only for you")
|
||||||
|
.setRequired(false)
|
||||||
}
|
}
|
||||||
})
|
],
|
||||||
|
run: async(ctx: InteractionCommandContext) => {
|
||||||
|
const hide = ctx.interaction.options.getBoolean("hide") ?? false;
|
||||||
|
|
||||||
|
await ctx.interaction.deferReply({
|
||||||
|
ephemeral: hide
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = ctx.interaction.options.getString("query");
|
||||||
|
const state: State = ctx.interaction.options.getString("state") as State || "all";
|
||||||
|
const type: Type = ctx.interaction.options.getString("type") as Type || "both";
|
||||||
|
|
||||||
|
const result = (await search(query, state, type))[0];
|
||||||
|
if (!result) {
|
||||||
|
ctx.interaction.editReply({
|
||||||
|
content: `❌ Couldn't find issue or pull request \`${query}\``
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.interaction.editReply({
|
||||||
|
content: [
|
||||||
|
`${result.emoji.type} ${result.emoji.state} [#${result.number} in oven-sh/bun](<${result.html_url}>) by [${result.user.login}](<${result.user.html_url}>) ${stateToText(result)} ${stateToTimestamp(result)}`,
|
||||||
|
result.title
|
||||||
|
].join("\n")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function stateToText(item: Item) {
|
||||||
|
switch (item.emoji.state) {
|
||||||
|
case "🔴":
|
||||||
|
case "🟠":
|
||||||
|
case "📝": {
|
||||||
|
return "opened";
|
||||||
|
}
|
||||||
|
case "🟢": {
|
||||||
|
return "closed as completed";
|
||||||
|
}
|
||||||
|
case "⚪️": {
|
||||||
|
return "closed as not planned";
|
||||||
|
}
|
||||||
|
case "⚫️": {
|
||||||
|
return "closed";
|
||||||
|
}
|
||||||
|
case "🟣": {
|
||||||
|
return "merged";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateToTimestamp(item: Item) {
|
||||||
|
let timestamp: Date;
|
||||||
|
|
||||||
|
switch (item.emoji.state) {
|
||||||
|
case "🔴":
|
||||||
|
case "🟠":
|
||||||
|
case "📝": {
|
||||||
|
timestamp = item.created_at;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "🟢":
|
||||||
|
case "⚪️":
|
||||||
|
case "⚫️": {
|
||||||
|
timestamp = item.closed_at;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "🟣": {
|
||||||
|
timestamp = item.pull_request.merged_at;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<t:${Math.round(timestamp.getTime() / 1000)}:R>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search(query: string, state: State, type: Type, length = 1): Promise<Item[]> {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
8
src/commands/index.ts
Normal file
8
src/commands/index.ts
Normal file
|
@ -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();
|
|
@ -1,16 +1,30 @@
|
||||||
import { InteractionResponseType, MessageFlags } from 'discord-api-types/v10';
|
import { defineCommand } from "../loaders/commands.ts";
|
||||||
import { Command } from '../structures/Command';
|
import { Bubu } from "../structs/Client.ts";
|
||||||
|
import { InteractionCommandContext, MessageCommandContext } from "../structs/context/CommandContext.ts";
|
||||||
|
|
||||||
new Command({
|
defineCommand({
|
||||||
name: 'ping',
|
name: "ping",
|
||||||
description: 'pong',
|
description: "pong",
|
||||||
run: (ctx) => {
|
run: async(ctx: InteractionCommandContext) => {
|
||||||
return ctx.respond({
|
const message = await ctx.interaction.deferReply({
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
ephemeral: true,
|
||||||
data: {
|
});
|
||||||
content: 'Pong 🏓',
|
|
||||||
flags: MessageFlags.Ephemeral,
|
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\``
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
|
@ -1,54 +1,89 @@
|
||||||
import {
|
import { GuildTextBasedChannel, SlashCommandStringOption, SlashCommandUserOption, User } from "discord.js";
|
||||||
APIApplicationCommandInteractionDataStringOption,
|
import { defineCommand } from "../loaders/commands.ts";
|
||||||
ApplicationCommandOptionType,
|
import { AutocompleteContext } from "../structs/context/AutocompleteContext.ts";
|
||||||
InteractionResponseType,
|
import { getTags, searchTag } from "../loaders/tags.ts";
|
||||||
MessageFlags,
|
import { InteractionCommandContext, MessageCommandContext } from "../structs/context/CommandContext.ts";
|
||||||
} from 'discord-api-types/v10';
|
import { Bubu } from "../structs/Client.ts";
|
||||||
import { Command } from '../structures/Command';
|
|
||||||
import { findTags, getTag } from '../utils/tagsUtils';
|
|
||||||
|
|
||||||
new Command({
|
defineCommand({
|
||||||
name: 'tag',
|
name: "tag",
|
||||||
description: 'Send a tag by name or alias',
|
description: "Get tag",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'query',
|
...new SlashCommandStringOption()
|
||||||
description: 'Tag name or alias',
|
.setName("query")
|
||||||
type: ApplicationCommandOptionType.String,
|
.setRequired(true)
|
||||||
required: true,
|
.setAutocomplete(true)
|
||||||
run: (ctx) => {
|
.setDescription("Select query")
|
||||||
return ctx.respond(findTags(ctx.value));
|
.toJSON(),
|
||||||
},
|
run: async(context: AutocompleteContext) => {
|
||||||
},
|
const query = context.options.getString("query");
|
||||||
{
|
if (!query) {
|
||||||
name: 'target',
|
return context.respond(getTags(context.channel, 25));
|
||||||
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;
|
|
||||||
|
|
||||||
const tag = getTag(query.value, false);
|
const tags = searchTag(context.channel, query, true);
|
||||||
if (!tag)
|
if (tags.length > 0)
|
||||||
return ctx.respond({
|
return context.respond(tags);
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: `\`❌\` Could not find a tag \`${query.value}\``,
|
|
||||||
flags: MessageFlags.Ephemeral,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return ctx.respond(
|
return context.respond(getTags(context.channel, 25));
|
||||||
[
|
},
|
||||||
target ? `*Tag suggestion for <@${target.id}>:*` : '',
|
|
||||||
tag.content,
|
|
||||||
].join('\n')
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
...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" ]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
import { InteractionResponseType, MessageFlags } from 'discord-api-types/v10';
|
import { defineCommand } from "../loaders/commands.ts";
|
||||||
import { Command } from '../structures/Command';
|
import { COMMIT_HASH, PRODUCTION } from "../constants.ts";
|
||||||
import { exec } from 'bun-utilities/spawn';
|
import { InteractionCommandContext } from "../structs/context/CommandContext.ts";
|
||||||
|
|
||||||
const commitHash = exec(['git', 'log', '--pretty=format:\'%h\'', '-n', '1']).stdout.replaceAll('\'', '');
|
export default defineCommand({
|
||||||
|
name: "version",
|
||||||
new Command({
|
description: "Show version",
|
||||||
name: 'version',
|
options: [],
|
||||||
description: 'Check bot and bun version',
|
run: (context: InteractionCommandContext) => {
|
||||||
run: (ctx) => {
|
context.interaction.reply({
|
||||||
return ctx.respond({
|
content: [
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
`[git-bun-discord-bot-${COMMIT_HASH}](<https://github.com/xHyroM/bun-discord-bot/tree/${COMMIT_HASH}>) ${!PRODUCTION ? "(dev)" : ""}`,
|
||||||
data: {
|
`[v${Bun.version} (${Bun.revision})](<https://github.com/oven-sh/bun/releases/tag/bun-v${Bun.version}>)`
|
||||||
content: [
|
].join("\n"),
|
||||||
`Bot version: [git-bun-discord-bot-"${commitHash}"](<https://github.com/xHyroM/bun-discord-bot/commit/${commitHash}>)`,
|
ephemeral: true,
|
||||||
`Bun version: [${process.version}](<https://github.com/oven-sh/bun/releases/tag/bun-${process.version}>)`,
|
});
|
||||||
].join('\n'),
|
}
|
||||||
flags: MessageFlags.Ephemeral,
|
});
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
11
src/constants.ts
Normal file
11
src/constants.ts
Normal file
|
@ -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" : "<>";
|
||||||
|
|
||||||
|
|
227
src/index.ts
227
src/index.ts
|
@ -1,222 +1,11 @@
|
||||||
import { Hono } from 'hono';
|
import "./loaders/tags.ts";
|
||||||
import { bodyParse } from 'hono/body-parse';
|
import "./commands";
|
||||||
import { Logger } from './utils/Logger';
|
import "./listeners";
|
||||||
|
|
||||||
// @ts-expect-error Types :(
|
import { Bubu } from "./structs/Client.ts";
|
||||||
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';
|
|
||||||
|
|
||||||
await fetchIssues();
|
// Make sure bubu will not crash
|
||||||
await fetchPullRequests();
|
process.on("unhandledRejection", console.error);
|
||||||
(async () => {
|
process.on("uncaughtException", console.error);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new Hono();
|
Bubu.login(process.env.DISCORD_BOT_TOKEN).catch(console.error);
|
||||||
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}`);
|
|
||||||
|
|
4
src/listeners/index.ts
Normal file
4
src/listeners/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import "./ready.ts";
|
||||||
|
import "./interaction_create.ts";
|
||||||
|
import "./message_create.ts";
|
||||||
|
import "./nickname_moderation.ts";
|
59
src/listeners/interaction_create.ts
Normal file
59
src/listeners/interaction_create.ts
Normal file
|
@ -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);
|
||||||
|
}
|
90
src/listeners/message_create.ts
Normal file
90
src/listeners/message_create.ts
Normal file
|
@ -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\/(?<repo>[a-zA-Z0-9-_]+\/[A-Za-z0-9_.-]+)\/blob\/(?<path>.+?)#L(?<first_line_number>\d+)[-~]?L?(?<second_line_number>\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<ButtonBuilder>()
|
||||||
|
.setComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setLabel(repo)
|
||||||
|
.setStyle(ButtonStyle.Link)
|
||||||
|
.setURL(`https://github.com/${repo}/blob/${path}#L${firstLineNumber + 1}${secondLineNumber ? `-L${secondLineNumber}` : ""}`)
|
||||||
|
)
|
||||||
|
.toJSON()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
25
src/listeners/nickname_moderation.ts
Normal file
25
src/listeners/nickname_moderation.ts
Normal file
|
@ -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"
|
||||||
|
}));
|
||||||
|
}
|
11
src/listeners/ready.ts
Normal file
11
src/listeners/ready.ts
Normal file
|
@ -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})`);
|
||||||
|
}
|
||||||
|
})
|
26
src/loaders/commands.ts
Normal file
26
src/loaders/commands.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { REST, Routes, SlashCommandBuilder } from "discord.js";
|
||||||
|
import type { Command } from "../structs/Command.ts";
|
||||||
|
|
||||||
|
export const COMMANDS: Map<string, Command> = new Map();
|
||||||
|
export const REST_CLIENT = new REST().setToken(process.env.DISCORD_TOKEN);
|
||||||
|
|
||||||
|
export function defineCommand<T extends Command>(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 ?? []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
14
src/loaders/listeners.ts
Normal file
14
src/loaders/listeners.ts
Normal file
|
@ -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<keyof ClientEvents>[] = [];
|
||||||
|
|
||||||
|
export function defineListener<T extends Listener<keyof ClientEvents>>(listener: T) {
|
||||||
|
LISTENERS.push(listener);
|
||||||
|
|
||||||
|
Bubu[listener.once ? "once" : "on"](
|
||||||
|
listener.event as keyof ClientEvents,
|
||||||
|
listener.run.bind(this)
|
||||||
|
);
|
||||||
|
}
|
102
src/loaders/tags.ts
Normal file
102
src/loaders/tags.ts
Normal file
|
@ -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<T extends boolean>(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;
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
import Collection from '@discordjs/collection';
|
|
||||||
import { Command } from '../structures/Command';
|
|
||||||
|
|
||||||
class CommandManager extends Collection<String, Command> {
|
|
||||||
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();
|
|
11
src/structs/Client.ts
Normal file
11
src/structs/Client.ts
Normal file
|
@ -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);
|
41
src/structs/Command.ts
Normal file
41
src/structs/Command.ts
Normal file
|
@ -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<true>
|
||||||
|
) => any;
|
||||||
|
runMessage?: (
|
||||||
|
context: CommandContext<false>
|
||||||
|
) => any;
|
||||||
|
}
|
10
src/structs/Listener.ts
Normal file
10
src/structs/Listener.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import type { ClientEvents } from "discord.js";
|
||||||
|
|
||||||
|
export interface Listener<E extends keyof ClientEvents> {
|
||||||
|
event: E;
|
||||||
|
once?: boolean;
|
||||||
|
run: (
|
||||||
|
...args: ClientEvents[E]
|
||||||
|
) => any;
|
||||||
|
}
|
||||||
|
|
8
src/structs/Tag.ts
Normal file
8
src/structs/Tag.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export interface Tag {
|
||||||
|
name: string;
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
keywords: string[];
|
||||||
|
category_ids: string[] | null;
|
||||||
|
channel_ids: string[] | null;
|
||||||
|
}
|
34
src/structs/context/AutocompleteContext.ts
Normal file
34
src/structs/context/AutocompleteContext.ts
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
72
src/structs/context/CommandContext.ts
Normal file
72
src/structs/context/CommandContext.ts
Normal file
|
@ -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<T extends boolean> {
|
||||||
|
command: Command;
|
||||||
|
|
||||||
|
user: User;
|
||||||
|
member: GuildMember | APIInteractionGuildMember;
|
||||||
|
channel: Channel;
|
||||||
|
|
||||||
|
reply(options: string | MessagePayload | (T extends true ? InteractionReplyOptions : MessageCreateOptions)): (T extends true ? Promise<Message | InteractionResponse> : Promise<Message>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InteractionCommandContext implements CommandContext<true> {
|
||||||
|
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<Message | InteractionResponse> {
|
||||||
|
return this.interaction.reply(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MessageCommandContext implements CommandContext<false> {
|
||||||
|
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<Message<boolean>> {
|
||||||
|
return this.channel.send(options);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<LocaleString, string>;
|
|
||||||
description?: string;
|
|
||||||
descriptionLocalizations?: Record<LocaleString, string>;
|
|
||||||
guildId?: string;
|
|
||||||
defaultMemberPermissions?: string;
|
|
||||||
options?: Option[] | OptionOptions[];
|
|
||||||
run: (ctx: CommandContext) => Response | Promise<Response>;
|
|
||||||
runEditResponse?: (ctx: CommandContext) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Command {
|
|
||||||
public name: string;
|
|
||||||
public nameLocalizations?: Record<LocaleString, string>;
|
|
||||||
public description?: string;
|
|
||||||
public descriptionLocalizations?: Record<LocaleString, string>;
|
|
||||||
public guildId?: string = config.client.guild_id;
|
|
||||||
public defaultMemberPermissions?: string;
|
|
||||||
public options: Option[] | OptionOptions[];
|
|
||||||
public run: (ctx: CommandContext) => Response | Promise<Response>;
|
|
||||||
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<string, any> {
|
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<LocaleString, string>;
|
|
||||||
value: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OptionOptions {
|
|
||||||
name: string;
|
|
||||||
nameLocalizations?: Record<LocaleString, string>;
|
|
||||||
description: string;
|
|
||||||
descriptionLocalizations?: Record<LocaleString, string>;
|
|
||||||
type: ApplicationCommandOptionType
|
|
||||||
required?: boolean;
|
|
||||||
choices?: OptionChoice[];
|
|
||||||
options?: Array<Option | OptionOptions>;
|
|
||||||
channelTypes?: ChannelType[];
|
|
||||||
minValue?: number;
|
|
||||||
maxValue?: number;
|
|
||||||
minLength?: number;
|
|
||||||
maxLength?: number;
|
|
||||||
run?: (ctx: AutocompleteContext) => Response | Promise<Response>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Option {
|
|
||||||
public name: string;
|
|
||||||
public nameLocalizations?: Record<LocaleString, string>;
|
|
||||||
public description: string;
|
|
||||||
public descriptionLocalizations?: Record<LocaleString, string>;
|
|
||||||
public type: ApplicationCommandOptionType;
|
|
||||||
public required?: boolean;
|
|
||||||
public choices?: Array<OptionChoice>;
|
|
||||||
public options?: Array<Option>;
|
|
||||||
public channelTypes?: Array<ChannelType | keyof typeof ChannelType>;
|
|
||||||
public minValue?: number;
|
|
||||||
public maxValue?: number;
|
|
||||||
public minLength?: number;
|
|
||||||
public maxLength?: number;
|
|
||||||
public run?: (ctx: AutocompleteContext) => Response | Promise<Response>;
|
|
||||||
|
|
||||||
public constructor(options: OptionOptions) {
|
|
||||||
this.name = options.name;
|
|
||||||
this.nameLocalizations = options.nameLocalizations;
|
|
||||||
this.description = options.description;
|
|
||||||
this.descriptionLocalizations = options.descriptionLocalizations;
|
|
||||||
this.type = options.type;
|
|
||||||
this.required = options.required;
|
|
||||||
this.choices = options.choices;
|
|
||||||
this.options = options.options?.map(argument => {
|
|
||||||
if (argument instanceof Option) return argument;
|
|
||||||
else return new Option(argument);
|
|
||||||
});
|
|
||||||
this.channelTypes = options.channelTypes;
|
|
||||||
this.minValue = options.minValue;
|
|
||||||
this.maxValue = options.maxValue;
|
|
||||||
this.minLength = options.minLength;
|
|
||||||
this.maxLength = options.maxLength;
|
|
||||||
this.run = options.run;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toJSON(): Record<string, any> {
|
|
||||||
if (
|
|
||||||
this.type === ApplicationCommandOptionType.Subcommand ||
|
|
||||||
this.type === ApplicationCommandOptionType.SubcommandGroup
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
name: this.name,
|
|
||||||
name_localizations: this.nameLocalizations,
|
|
||||||
description: this.description,
|
|
||||||
description_localizations: this.descriptionLocalizations,
|
|
||||||
type: this.type,
|
|
||||||
options: this.options?.map(option => option.toJSON()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: this.name,
|
|
||||||
name_localizations: this.nameLocalizations,
|
|
||||||
description: this.description,
|
|
||||||
description_localizations: this.descriptionLocalizations,
|
|
||||||
type: this.type,
|
|
||||||
required: this.required,
|
|
||||||
choices: this.choices,
|
|
||||||
channel_types: this.channelTypes,
|
|
||||||
min_value: this.minValue,
|
|
||||||
max_value: this.maxValue,
|
|
||||||
min_length: this.minLength,
|
|
||||||
max_length: this.maxLength,
|
|
||||||
autocomplete: typeof this.run === 'function',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { APIApplicationCommandInteractionDataBasicOption, APIApplicationCommandOptionChoice, InteractionResponseType } from 'discord-api-types/v10';
|
|
||||||
import { Context } from 'hono';
|
|
||||||
import { Option, OptionOptions } from '../Option';
|
|
||||||
|
|
||||||
export class AutocompleteContext {
|
|
||||||
public context: Context;
|
|
||||||
public option?: Option | OptionOptions;
|
|
||||||
public value?: string;
|
|
||||||
public options?: APIApplicationCommandInteractionDataBasicOption[];
|
|
||||||
|
|
||||||
public constructor(c: Context, option: Option | OptionOptions, value: string, options: APIApplicationCommandInteractionDataBasicOption[]) {
|
|
||||||
this.context = c;
|
|
||||||
this.option = option;
|
|
||||||
this.value = value;
|
|
||||||
this.options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
public respond(response: APIApplicationCommandOptionChoice[]) {
|
|
||||||
return this.context.json({
|
|
||||||
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
|
|
||||||
data: {
|
|
||||||
choices: response
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { APIApplicationCommandInteraction, APIApplicationCommandInteractionDataOption, APIChatInputApplicationCommandInteraction, APIChatInputApplicationCommandInteractionDataResolved, APIInteractionGuildMember, APIInteractionResponse, APIInteractionResponseCallbackData, APIUser, ApplicationCommandType, InteractionResponseType, InteractionType, RouteBases, Routes } from 'discord-api-types/v10';
|
|
||||||
import { Context } from 'hono';
|
|
||||||
import { Command } from '../Command';
|
|
||||||
// @ts-expect-error Types :(
|
|
||||||
import config from '../../../files/config.toml';
|
|
||||||
|
|
||||||
export class CommandContext {
|
|
||||||
public context: Context;
|
|
||||||
public command?: Command;
|
|
||||||
public interaction?: APIChatInputApplicationCommandInteraction;
|
|
||||||
public user?: APIUser;
|
|
||||||
public member?: APIInteractionGuildMember;
|
|
||||||
public options?: APIApplicationCommandInteractionDataOption[];
|
|
||||||
public resolved?: APIChatInputApplicationCommandInteractionDataResolved;
|
|
||||||
|
|
||||||
public constructor(c: Context, command?: Command, interaction?: APIApplicationCommandInteraction) {
|
|
||||||
this.context = c;
|
|
||||||
this.command = command;
|
|
||||||
|
|
||||||
if (interaction) {
|
|
||||||
this.interaction = interaction as APIChatInputApplicationCommandInteraction;
|
|
||||||
this.user = this.interaction.member.user;
|
|
||||||
this.member = this.interaction.member;
|
|
||||||
this.options = this.interaction.data.options;
|
|
||||||
this.resolved = this.interaction.data.resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public respond(response: APIInteractionResponse | string) {
|
|
||||||
if (typeof response === 'string') {
|
|
||||||
return this.context.json({
|
|
||||||
type: InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: response
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.context.json(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async editResponse(response: APIInteractionResponseCallbackData | string) {
|
|
||||||
if (typeof response === 'string') {
|
|
||||||
response = {
|
|
||||||
content: response
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(`${RouteBases.api}${Routes.webhookMessage(this.interaction.application_id, this.interaction.token)}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bot ${config.client.token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
10
src/util.ts
Normal file
10
src/util.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export function safeSlice<T>(input: T, length: number) {
|
||||||
|
// @ts-expect-error i know where im using it
|
||||||
|
return input.length > length ? input.slice(0, length) : input;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function silently<T>(value: Promise<T>) {
|
||||||
|
try {
|
||||||
|
await value;
|
||||||
|
} catch {}
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
type Level = 'INFO' | 'SUCCESS' | 'WARN' | 'ERROR' | 'DEBUG'
|
|
||||||
|
|
||||||
export class Logger extends null {
|
|
||||||
public static info(...messages: any[]) {
|
|
||||||
this.log('INFO', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static success(...messages: any[]) {
|
|
||||||
this.log('SUCCESS', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static warn(...messages: any[]) {
|
|
||||||
this.log('WARN', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static error(...messages: any[]) {
|
|
||||||
this.log('ERROR', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static debug(...messages: any[]) {
|
|
||||||
this.log('DEBUG', messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static log(level: Level, messages: any[]) {
|
|
||||||
const date = new Date();
|
|
||||||
|
|
||||||
let color = '';
|
|
||||||
switch (level) {
|
|
||||||
case 'INFO':
|
|
||||||
color = '\x1b[36m';
|
|
||||||
break;
|
|
||||||
case 'SUCCESS':
|
|
||||||
color = '\u001b[32m';
|
|
||||||
break;
|
|
||||||
case 'WARN':
|
|
||||||
color = '\x1b[93m';
|
|
||||||
break;
|
|
||||||
case 'ERROR':
|
|
||||||
color = '\x1b[91m';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
color = '\x1b[2m';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`${color}[${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}/${level}]\x1b[0m`, ...messages);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { Logger } from "./Logger";
|
|
||||||
|
|
||||||
export const getDiscordGuildMembers = async(token: string) => {
|
|
||||||
let oldId;
|
|
||||||
const result: any[] = [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const members: any[] = await (await fetch(
|
|
||||||
`https://discord.com/api/v10/guilds/876711213126520882/members?limit=1000${oldId ? `&after=${oldId}` : ''}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)).json();
|
|
||||||
|
|
||||||
if (members.length == 0) break;
|
|
||||||
|
|
||||||
console.log(members);
|
|
||||||
result.push(...members.map(m => ({ id: m.id, nickname: m.nick })));
|
|
||||||
oldId = members[members.length - 1].id;
|
|
||||||
|
|
||||||
Logger.debug(`Fetching guild members - ${result.length}, ${oldId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`All guild members has been fetched - ${result.length}`);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const removeExclamationFromNicknames = async(token: string) => {
|
|
||||||
for (const member of await getDiscordGuildMembers(token)) {
|
|
||||||
if (!member.nickname?.startsWith?.('!')) continue;
|
|
||||||
|
|
||||||
await fetch(`https://discord.com/api/v8/guilds/876711213126520882/members/${member.id}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
nick: member.nickname.slice(1),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,270 +0,0 @@
|
||||||
// @ts-expect-error Types :(
|
|
||||||
import config from '../../files/config.toml';
|
|
||||||
// @ts-expect-error Types :(
|
|
||||||
import utilities from '../../files/utilities.toml';
|
|
||||||
import MiniSearch from 'minisearch';
|
|
||||||
import { Logger } from './Logger';
|
|
||||||
import { APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
|
|
||||||
import { Database } from 'bun:sqlite';
|
|
||||||
import { githubTitleClean } from './regexes';
|
|
||||||
|
|
||||||
export type IssueState = 'open' | 'closed' | 'all' | 'merged';
|
|
||||||
export type IssueType = '(IS)' | '(PR)' | '(IS|PR)';
|
|
||||||
interface Issue {
|
|
||||||
id: number;
|
|
||||||
repository: string;
|
|
||||||
title: string;
|
|
||||||
number: number;
|
|
||||||
state: IssueState,
|
|
||||||
created_at: string;
|
|
||||||
closed_at: string | null;
|
|
||||||
html_url: string;
|
|
||||||
user_login: string;
|
|
||||||
user_html_url: string;
|
|
||||||
type: IssueType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PullRequest extends Issue {
|
|
||||||
merged_at: string | null;
|
|
||||||
draft: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const db = new Database('./files/database.sqlite');
|
|
||||||
await db.exec('DROP TABLE IF EXISTS issuesandprs');
|
|
||||||
await db.exec('CREATE TABLE issuesandprs (id INTEGER PRIMARY KEY, repository TEXT, title TEXT, number INTEGER, state TEXT, created_at TEXT, closed_at TEXT, merged_at TEXT, html_url TEXT, user_login TEXT, user_html_url TEXT, type TEXT, draft TINYINT)');
|
|
||||||
|
|
||||||
const addToDb = db.prepare(
|
|
||||||
'INSERT INTO issuesandprs (repository, title, number, state, created_at, closed_at, merged_at, html_url, user_login, user_html_url, type, draft) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
||||||
);
|
|
||||||
|
|
||||||
export let issues: number = 0;
|
|
||||||
export let pulls: number = 0;
|
|
||||||
|
|
||||||
export const fetchIssues = async() => {
|
|
||||||
for await (const repository of utilities.github.repositories) {
|
|
||||||
let page = 1;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const res = await (await fetch(`https://api.github.com/repos/${repository}/issues?per_page=100&page=${page}&state=all`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'bun-discord-bot',
|
|
||||||
'Authorization': `token ${config.api.github_personal_access_token}`
|
|
||||||
}
|
|
||||||
})).json() as any;
|
|
||||||
|
|
||||||
for (const issue of res) {
|
|
||||||
if ('pull_request' in issue) continue;
|
|
||||||
|
|
||||||
// @ts-expect-error it works
|
|
||||||
await addToDb.run([
|
|
||||||
issue.repository_url.replace('https://api.github.com/repos/', ''),
|
|
||||||
issue.title,
|
|
||||||
issue.number,
|
|
||||||
issue.state,
|
|
||||||
issue.created_at,
|
|
||||||
issue.closed_at,
|
|
||||||
null,
|
|
||||||
issue.html_url,
|
|
||||||
issue.user.login,
|
|
||||||
issue.user.html_url,
|
|
||||||
'(IS)',
|
|
||||||
null,
|
|
||||||
]);
|
|
||||||
issues++;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`Fetching issues for ${repository} - ${issues} * ${page}`);
|
|
||||||
|
|
||||||
page++;
|
|
||||||
if (res.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.success(`Issues have been fetched for ${repository} - ${issues}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
issues = null;
|
|
||||||
Object.freeze(issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchPullRequests = async() => {
|
|
||||||
for await (const repository of utilities.github.repositories) {
|
|
||||||
let page = 1;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const res = await (await fetch(`https://api.github.com/repos/${repository}/pulls?per_page=100&page=${page}&state=all`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'bun-discord-bot',
|
|
||||||
'Authorization': `token ${config.api.github_personal_access_token}`
|
|
||||||
}
|
|
||||||
})).json() as any;
|
|
||||||
|
|
||||||
for (const pull of res) {
|
|
||||||
// @ts-expect-error it works
|
|
||||||
await addToDb.run([
|
|
||||||
pull.html_url.replace('https://github.com/', '').replace(`/pull/${pull.number}`, ''),
|
|
||||||
pull.title,
|
|
||||||
pull.number,
|
|
||||||
pull.state,
|
|
||||||
pull.created_at,
|
|
||||||
pull.closed_at,
|
|
||||||
pull.merged_at,
|
|
||||||
pull.html_url,
|
|
||||||
pull.user.login,
|
|
||||||
pull.user.html_url,
|
|
||||||
'(PR)',
|
|
||||||
pull.draft,
|
|
||||||
]);
|
|
||||||
pulls++;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`Fetching pull requests for ${repository} - ${pulls} * ${page}`);
|
|
||||||
|
|
||||||
page++;
|
|
||||||
if (res.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.success(`Pull requests have been fetched for ${repository} - ${pulls}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
pulls = null;
|
|
||||||
Object.freeze(pulls);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setIssue = async(issue: Issue) => {
|
|
||||||
const exists = await db.prepare(`SELECT * FROM issuesandprs WHERE number = ? AND repository = ?`).get(issue.number, issue.repository);
|
|
||||||
if (exists) {
|
|
||||||
db.exec(`UPDATE issuesandprs SET state = '${issue.state}', closed_at = '${issue.closed_at}', title = '${issue.title}' WHERE number = ${issue.number} AND repository = '${issue.repository}'`);
|
|
||||||
} else {
|
|
||||||
// @ts-expect-error
|
|
||||||
addToDb.run([
|
|
||||||
issue.repository,
|
|
||||||
issue.title,
|
|
||||||
issue.number,
|
|
||||||
issue.state,
|
|
||||||
issue.created_at,
|
|
||||||
issue.closed_at,
|
|
||||||
null,
|
|
||||||
issue.html_url,
|
|
||||||
issue.user_login,
|
|
||||||
issue.user_html_url,
|
|
||||||
'(IS)',
|
|
||||||
null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setPullRequest = async(pull: PullRequest) => {
|
|
||||||
const exists = await db.prepare(`SELECT * FROM issuesandprs WHERE number = ? AND repository = ?`).get(pull.number, pull.repository);
|
|
||||||
if (exists) {
|
|
||||||
db.exec(`UPDATE issuesandprs SET state = '${pull.state}', closed_at = '${pull.closed_at}', merged_at = '${pull.merged_at}', title = '${pull.title}' WHERE number = ${pull.number} AND repository = '${pull.repository}'`);
|
|
||||||
} else {
|
|
||||||
// @ts-expect-error
|
|
||||||
addToDb.run([
|
|
||||||
pull.repository,
|
|
||||||
pull.title,
|
|
||||||
pull.number,
|
|
||||||
pull.state,
|
|
||||||
pull.created_at,
|
|
||||||
pull.closed_at,
|
|
||||||
pull.merged_at,
|
|
||||||
pull.html_url,
|
|
||||||
pull.user_login,
|
|
||||||
pull.user_html_url,
|
|
||||||
'(IS)',
|
|
||||||
pull.draft,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteIssueOrPR = (number: number, repository: string) => {
|
|
||||||
db.exec(`DELETE FROM issuesandprs WHERE repository = '${repository}' AND number = ${number}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const search = async(query: string, repository: string, state: IssueState, type: IssueType): Promise<APIApplicationCommandOptionChoice[]> => {
|
|
||||||
try {
|
|
||||||
const sqliteTypePrepase = type !== '(IS|PR)' ? ` AND type = '${type}'` : '';
|
|
||||||
const arrayFiltered = state === 'all'
|
|
||||||
? await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ?${sqliteTypePrepase}`).all(repository)
|
|
||||||
: state === 'merged'
|
|
||||||
? await db.prepare(`SELECT * FROM issuesandprs WHERE merged_at IS NOT NULL AND repository = ?${sqliteTypePrepase}`).all(repository)
|
|
||||||
: await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ? AND state = ?${sqliteTypePrepase}`).all(repository, state);
|
|
||||||
|
|
||||||
if (!query) {
|
|
||||||
const array = arrayFiltered.slice(0, 25);
|
|
||||||
return array.map((issueOrPr: Issue | PullRequest) => new Object({
|
|
||||||
name: `${issueOrPr.type.slice(0, -1)} #${issueOrPr.number}) ${formatEmojiStatus(issueOrPr)} ${issueOrPr.title.slice(0, 91 - issueOrPr.id.toString().length).replace(githubTitleClean, '')}`,
|
|
||||||
value: issueOrPr.number.toString()
|
|
||||||
})) as APIApplicationCommandOptionChoice[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const searcher = new MiniSearch({
|
|
||||||
fields: query.startsWith('#') ? ['number'] : ['title'],
|
|
||||||
storeFields: ['title', 'number', 'type', 'state', 'merged_at', 'draft'],
|
|
||||||
searchOptions: {
|
|
||||||
fuzzy: 3,
|
|
||||||
processTerm: term => term.toLowerCase(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
searcher.addAll(arrayFiltered);
|
|
||||||
|
|
||||||
const result = searcher.search(query);
|
|
||||||
|
|
||||||
return (result as unknown as Issue[] | PullRequest[]).slice(0, 25).map((issueOrPr: Issue | PullRequest) => new Object({
|
|
||||||
name: `${issueOrPr.type.slice(0, -1)} #${issueOrPr.number}) ${formatEmojiStatus(issueOrPr)} ${issueOrPr.title.slice(0, 91 - issueOrPr.id.toString().length).replace(githubTitleClean, '')}`,
|
|
||||||
value: issueOrPr.number.toString()
|
|
||||||
})) as APIApplicationCommandOptionChoice[]
|
|
||||||
} catch(e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getIssueOrPR = async(number: number, repository: string, state: IssueState, type: IssueType): Promise<Issue | PullRequest> => {
|
|
||||||
const sqliteTypePrepase = type !== '(IS|PR)' ? ` AND type = '${type}'` : '';
|
|
||||||
const issueOrPR = state === 'all'
|
|
||||||
? await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ? AND number = ?${sqliteTypePrepase}`).get(repository, number)
|
|
||||||
: state === 'merged'
|
|
||||||
? await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ? AND number = ? AND merged_at IS NOT NULL${sqliteTypePrepase}`).get(repository, number)
|
|
||||||
: await db.prepare(`SELECT * FROM issuesandprs WHERE repository = ? AND number = ? AND state = ?${sqliteTypePrepase}`).get(repository, number, state);
|
|
||||||
|
|
||||||
return issueOrPR;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatStatus = (data: Issue | PullRequest) => {
|
|
||||||
let operation = '';
|
|
||||||
let timestamp = '';
|
|
||||||
switch(data.state as 'open' | 'closed' | 'all') {
|
|
||||||
case 'open':
|
|
||||||
operation = 'opened';
|
|
||||||
timestamp = `<t:${Math.floor(new Date(data.created_at).getTime() / 1000)}:R>`;
|
|
||||||
break;
|
|
||||||
case 'closed':
|
|
||||||
operation = (data as PullRequest).merged_at ? 'merged' : 'closed';
|
|
||||||
timestamp = (data as PullRequest).merged_at
|
|
||||||
? `<t:${Math.floor(new Date((data as PullRequest).merged_at).getTime() / 1000)}:R>`
|
|
||||||
: `<t:${Math.floor(new Date(data.closed_at).getTime() / 1000)}:R>`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${operation} ${timestamp}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatEmojiStatus = (data: Issue | PullRequest) => {
|
|
||||||
let emoji = '';
|
|
||||||
switch(data.state as 'open' | 'closed' | 'all') {
|
|
||||||
case 'open':
|
|
||||||
emoji = (data as PullRequest).draft ? '⚫' : '🟢';
|
|
||||||
break;
|
|
||||||
case 'closed':
|
|
||||||
emoji = (data as PullRequest).merged_at ? '🟣' : '🔴';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return emoji;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { readdirSync } from 'fs';
|
|
||||||
import { basename, dirname, join } from 'path';
|
|
||||||
import { Logger } from './Logger';
|
|
||||||
|
|
||||||
const __dirname = new URL('.', import.meta.url).pathname;
|
|
||||||
|
|
||||||
export default async() => {
|
|
||||||
const commandsDir = join(__dirname, '..', 'commands');
|
|
||||||
for (
|
|
||||||
const command of readdirSync(commandsDir)
|
|
||||||
) {
|
|
||||||
const name = basename(command, '.ts');
|
|
||||||
Logger.info(`Loading ${name} command`);
|
|
||||||
await import(join(commandsDir, command));
|
|
||||||
Logger.success(`Command ${name} has been loaded`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export const githubTitleClean = /[`]/gi;
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { RouteBases, Routes } from 'discord-api-types/v10';
|
|
||||||
import { Commands } from '../managers/CommandManager';
|
|
||||||
import { Command } from '../structures/Command';
|
|
||||||
import { Logger } from './Logger';
|
|
||||||
|
|
||||||
const sync = async(
|
|
||||||
clientToken: string,
|
|
||||||
clientUserId: string,
|
|
||||||
commands: Command[],
|
|
||||||
guildId?: string,
|
|
||||||
) => {
|
|
||||||
const res = await fetch(
|
|
||||||
`${RouteBases.api}${guildId ? Routes.applicationGuildCommands(clientUserId, guildId) : Routes.applicationCommands(clientUserId)}`,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(commands.flatMap(command => command.toJSON())),
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bot ${clientToken}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (res.ok) return Logger.success('🌍 All commands have been synchronized with discord api.');
|
|
||||||
const data = await res.json() as any;
|
|
||||||
|
|
||||||
if (res.status === 429) {
|
|
||||||
setTimeout(
|
|
||||||
() => sync(clientToken, clientUserId, commands, guildId),
|
|
||||||
data.retry_after * 1000,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Logger.error(
|
|
||||||
typeof data.code !== 'undefined' ? data.code.toString() : '',
|
|
||||||
data.message
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async(clientToken: string, clientUserId: string) => {
|
|
||||||
if (Commands.size === 0) return;
|
|
||||||
|
|
||||||
const [guild, global] = Commands.partition(
|
|
||||||
command => typeof command.guildId === 'string',
|
|
||||||
);
|
|
||||||
|
|
||||||
const guildIds = new Set(guild.map(c => c.guildId));
|
|
||||||
for await (const guildId of guildIds) {
|
|
||||||
const commands = guild.filter(item => item.guildId === guildId);
|
|
||||||
await sync(clientToken, clientUserId, [...commands.values()], guildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.size > 0) await sync(clientToken, clientUserId, [...global.values()]);
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import Collection from '@discordjs/collection';
|
|
||||||
import { APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
|
|
||||||
// @ts-expect-error Types :(
|
|
||||||
import tags from '../../files/tags.toml';
|
|
||||||
|
|
||||||
export interface Tag {
|
|
||||||
keywords: string[];
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagCache: Collection<string, Tag> = new Collection();
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(tags)) {
|
|
||||||
(value as Tag).content = (value as Tag).content.replaceAll('+', '`');
|
|
||||||
tagCache.set(key, value as unknown as Tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTag = <T extends boolean>(name: string, more?: T): T extends true ? APIApplicationCommandOptionChoice[] : Tag => {
|
|
||||||
if (more) {
|
|
||||||
const exactKeywords: APIApplicationCommandOptionChoice[] = [];
|
|
||||||
const keywordMatches: APIApplicationCommandOptionChoice[] = [];
|
|
||||||
const contentMatches: APIApplicationCommandOptionChoice[] = [];
|
|
||||||
const query = name.toLowerCase();
|
|
||||||
|
|
||||||
for (const [tagName, tag] of tagCache.entries()) {
|
|
||||||
const exactKeyword = tag.keywords.find((t) => t.toLowerCase() === query);
|
|
||||||
const includesKeyword = tag.keywords.find((t) => t.toLowerCase().includes(query));
|
|
||||||
const contentMatch = tag.content.toLowerCase().includes(query);
|
|
||||||
|
|
||||||
if (exactKeyword) {
|
|
||||||
exactKeywords.push({
|
|
||||||
name: `✅ ${tagName.replaceAll('-', ' ')}`,
|
|
||||||
value: tagName
|
|
||||||
});
|
|
||||||
} else if (includesKeyword) {
|
|
||||||
keywordMatches.push({
|
|
||||||
name: `🔑 ${tagName.replaceAll('-', ' ')}`,
|
|
||||||
value: tagName
|
|
||||||
});
|
|
||||||
} else if (contentMatch) {
|
|
||||||
contentMatches.push({
|
|
||||||
name: `📄 ${tagName.replaceAll('-', ' ')}`,
|
|
||||||
value: tagName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = [...exactKeywords, ...keywordMatches, ...contentMatches];
|
|
||||||
return tags as T extends true ? APIApplicationCommandOptionChoice[] : Tag;
|
|
||||||
} else {
|
|
||||||
const tag = tagCache.get(name) || tagCache.find(tag => tag.keywords.some(k => k.includes(name)));
|
|
||||||
return tag as T extends true ? APIApplicationCommandOptionChoice[] : Tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const findTags = (name: string) => {
|
|
||||||
if (!name)
|
|
||||||
return [
|
|
||||||
...tagCache.map((tag, name) => new Object({
|
|
||||||
name: `🚀 ${name.replaceAll('-', ' ')}`,
|
|
||||||
value: name
|
|
||||||
})).slice(0, 25)
|
|
||||||
];
|
|
||||||
else {
|
|
||||||
const tags = getTag(name, true);
|
|
||||||
if (tags.length > 0) return tags;
|
|
||||||
else return findTags(null);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
// from https://github.com/discord/discord-interactions-js/blob/main/src/index.ts
|
|
||||||
|
|
||||||
import { sign } from 'tweetnacl';
|
|
||||||
import createHmac from 'create-hmac';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts different types to Uint8Array.
|
|
||||||
*
|
|
||||||
* @param value - Value to convert. Strings are parsed as hex.
|
|
||||||
* @param format - Format of value. Valid options: 'hex'. Defaults to utf-8.
|
|
||||||
* @returns Value in Uint8Array form.
|
|
||||||
*/
|
|
||||||
function valueToUint8Array(value: Uint8Array | ArrayBuffer | Buffer | string, format?: string): Uint8Array {
|
|
||||||
if (value == null) {
|
|
||||||
return new Uint8Array();
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
if (format === 'hex') {
|
|
||||||
const matches = value.match(/.{1,2}/g);
|
|
||||||
if (matches == null) {
|
|
||||||
throw new Error('Value is not a valid hex string');
|
|
||||||
}
|
|
||||||
const hexVal = matches.map((byte: string) => parseInt(byte, 16));
|
|
||||||
return new Uint8Array(hexVal);
|
|
||||||
} else {
|
|
||||||
return new TextEncoder().encode(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (Buffer.isBuffer(value)) {
|
|
||||||
const arrayBuffer = value.buffer.slice(value.byteOffset, value.byteOffset + value.length);
|
|
||||||
return new Uint8Array(value);
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
// Runtime doesn't have Buffer
|
|
||||||
}
|
|
||||||
if (value instanceof ArrayBuffer) {
|
|
||||||
return new Uint8Array(value);
|
|
||||||
}
|
|
||||||
if (value instanceof Uint8Array) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
throw new Error('Unrecognized value type, must be one of: string, Buffer, ArrayBuffer, Uint8Array');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge two arrays.
|
|
||||||
*
|
|
||||||
* @param arr1 - First array
|
|
||||||
* @param arr2 - Second array
|
|
||||||
* @returns Concatenated arrays
|
|
||||||
*/
|
|
||||||
function concatUint8Arrays(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {
|
|
||||||
const merged = new Uint8Array(arr1.length + arr2.length);
|
|
||||||
merged.set(arr1);
|
|
||||||
merged.set(arr2, arr1.length);
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a payload from Discord against its signature and key.
|
|
||||||
*
|
|
||||||
* @param rawBody - The raw payload data
|
|
||||||
* @param signature - The signature from the `X-Signature-Ed25519` header
|
|
||||||
* @param timestamp - The timestamp from the `X-Signature-Timestamp` header
|
|
||||||
* @param clientPublicKey - The public key from the Discord developer dashboard
|
|
||||||
* @returns Whether or not validation was successful
|
|
||||||
*/
|
|
||||||
export const verifyKey = (
|
|
||||||
body: Uint8Array | ArrayBuffer | Buffer | string,
|
|
||||||
signature: Uint8Array | ArrayBuffer | Buffer | string,
|
|
||||||
timestamp: Uint8Array | ArrayBuffer | Buffer | string,
|
|
||||||
clientPublicKey: Uint8Array | ArrayBuffer | Buffer | string,
|
|
||||||
): boolean => {
|
|
||||||
try {
|
|
||||||
const timestampData = valueToUint8Array(timestamp);
|
|
||||||
const bodyData = valueToUint8Array(body);
|
|
||||||
const message = concatUint8Arrays(timestampData, bodyData);
|
|
||||||
|
|
||||||
const signatureData = valueToUint8Array(signature, 'hex');
|
|
||||||
const publicKeyData = valueToUint8Array(clientPublicKey, 'hex');
|
|
||||||
return sign.detached.verify(message, signatureData, publicKeyData);
|
|
||||||
} catch (ex) {
|
|
||||||
console.error('[discord-interactions]: Invalid verifyKey parameters', ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a payload from GitHub against its signature and secret
|
|
||||||
*/
|
|
||||||
export const verifyGithubKey = (
|
|
||||||
body: string,
|
|
||||||
signature: string,
|
|
||||||
secret: string
|
|
||||||
): boolean => {
|
|
||||||
if (!body || !signature || !secret) return false;
|
|
||||||
|
|
||||||
const githubWebhooksSecret = new TextEncoder().encode(secret);
|
|
||||||
const sha256 = `sha256=${createHmac('sha256', githubWebhooksSecret).update(body).digest('hex')}`;
|
|
||||||
if (sha256 !== signature) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
|
@ -1,10 +1,12 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
// "bun-types" is the important part
|
"allowImportingTsExtensions": true,
|
||||||
"types": ["bun-types"]
|
"allowSyntheticDefaultImports": true,
|
||||||
}
|
// "bun-types" is the important part
|
||||||
|
"types": ["bun-types"]
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue