Initial Commit
This commit is contained in:
26
packages/discord/.github/workflows/release.yml
vendored
Normal file
26
packages/discord/.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- run: npx changelogithub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
38
packages/discord/.github/workflows/unit-test.yml
vendored
Normal file
38
packages/discord/.github/workflows/unit-test.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Unit Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
||||
- name: Set node LTS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: pnpm
|
||||
|
||||
- name: Install
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Test
|
||||
run: pnpm run test
|
||||
4
packages/discord/.gitignore
vendored
Normal file
4
packages/discord/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
.DS_Store
|
||||
8
packages/discord/.prettierrc.yaml
Normal file
8
packages/discord/.prettierrc.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
trailingComma: all
|
||||
tabWidth: 2
|
||||
useTabs: false
|
||||
semi: true
|
||||
singleQuote: true
|
||||
printWidth: 140
|
||||
experimentalTernaries: true
|
||||
quoteProps: consistent
|
||||
3
packages/discord/.vscode/settings.json
vendored
Normal file
3
packages/discord/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
23
packages/discord/README.md
Normal file
23
packages/discord/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# tsdown-starter
|
||||
|
||||
A starter for creating a TypeScript package.
|
||||
|
||||
## Development
|
||||
|
||||
- Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
- Run the unit tests:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
- Build the library:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
215
packages/discord/bun.lock
Normal file
215
packages/discord/bun.lock
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "tsdown-starter",
|
||||
"dependencies": {
|
||||
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.21",
|
||||
"@types/node": "^22.15.17",
|
||||
"bumpp": "^10.1.0",
|
||||
"tsdown": "^0.11.9",
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.70.0", "", {}, "sha512-ngyLUpUjO3dpqygSRQDx7nMx8+BmXbWOU4oIwTJFV2MVIDG7knIZwgdwXlQWLg3C3oxg1lS7ppMtPKqKFb7wzw=="],
|
||||
|
||||
"@projectdysnomia/dysnomia": ["@projectdysnomia/dysnomia@github:projectdysnomia/dysnomia#5e3300e", { "dependencies": { "ws": "^8.18.0" }, "optionalDependencies": { "@stablelib/xchacha20poly1305": "~1.0.1", "opusscript": "^0.1.1" }, "peerDependencies": { "@discordjs/opus": "^0.9.0", "erlpack": "github:discord/erlpack", "eventemitter3": "^5.0.1", "pako": "^2.1.0", "sodium-native": "^4.1.1", "zlib-sync": "^0.1.9" }, "optionalPeers": ["@discordjs/opus", "erlpack", "eventemitter3", "pako", "sodium-native", "zlib-sync"] }, "projectdysnomia-dysnomia-5e3300e"],
|
||||
|
||||
"@quansync/fs": ["@quansync/fs@0.1.5", "", { "dependencies": { "quansync": "^0.2.11" } }, "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-geUG/FUpm+membLC0NQBb39vVyOfguYZ2oyXc7emr6UjH6TeEECT4b0CPZXKFnELareTiU/Jfl70/eEgNxyQeA=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-7wPXDwcOtv2I+pWTL2UNpNAxMAGukgBT90Jz4DCfwaYdGvQncF7J0S7IWrRVsRFhBavxM+65RcueE3VXw5UIbg=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-agO5mONTNKVrcIt4SRxw5Ni0FOVV3gaH8dIiNp1A4JeU91b9kw7x+JRuNJAQuM2X3pYqVvA6qh13UTNOsaqM/Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.9", "", { "os": "linux", "cpu": "arm" }, "sha512-dDNDV9p/8WYDriS9HCcbH6y6+JP38o3enj/pMkdkmkxEnZ0ZoHIfQ9RGYWeRYU56NKBCrya4qZBJx49Jk9LRug=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-kZKegmHG1ZvfsFIwYU6DeFSxSIcIliXzeznsJHUo9D9/dlVSDi/PUvsRKcuJkQjZoejM6pk8MHN/UfgGdIhPHw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-f+VL8mO31pyMJiJPr2aA1ryYONkP2UqgbwK7fKtKHZIeDd/AoUGn3+ujPqDhuy2NxgcJ5H8NaSvDpG1tJMHh+g=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.9", "", { "os": "linux", "cpu": "x64" }, "sha512-GiUEZ0WPjX5LouDoC3O8aJa4h6BLCpIvaAboNw5JoRour/3dC6rbtZZ/B5FC3/ySsN3/dFOhAH97ylQxoZJi7A=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.9", "", { "os": "linux", "cpu": "x64" }, "sha512-AMb0dicw+QHh6RxvWo4BRcuTMgS0cwUejJRMpSyIcHYnKTbj6nUW4HbWNQuDfZiF27l6F5gEwBS+YLUdVzL9vg=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.9", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.4" }, "cpu": "none" }, "sha512-+pdaiTx7L8bWKvsAuCE0HAxP1ze1WOLoWGCawcrZbMSY10dMh2i82lJiH6tXGXbfYYwsNWhWE2NyG4peFZvRfQ=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-A7kN248viWvb8eZMzQu024TBKGoyoVYBsDG2DtoP8u2pzwoh5yDqUL291u01o4f8uzpUHq8mfwQJmcGChFu8KQ=="],
|
||||
|
||||
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-DzKN7iEYjAP8AK8F2G2aCej3fk43Y/EQrVrR3gF0XREes56chjQ7bXIhw819jv74BbxGdnpPcslhet/cgt7WRA=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.9", "", { "os": "win32", "cpu": "x64" }, "sha512-GMWgTvvbZ8TfBsAiJpoz4SRq3IN3aUMn0rYm8q4I8dcEk4J1uISyfb6ZMzvqW+cvScTWVKWZNqnrmYOKLLUt4w=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="],
|
||||
|
||||
"@stablelib/aead": ["@stablelib/aead@1.0.1", "", {}, "sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg=="],
|
||||
|
||||
"@stablelib/binary": ["@stablelib/binary@1.0.1", "", { "dependencies": { "@stablelib/int": "^1.0.1" } }, "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q=="],
|
||||
|
||||
"@stablelib/chacha": ["@stablelib/chacha@1.0.1", "", { "dependencies": { "@stablelib/binary": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg=="],
|
||||
|
||||
"@stablelib/chacha20poly1305": ["@stablelib/chacha20poly1305@1.0.1", "", { "dependencies": { "@stablelib/aead": "^1.0.1", "@stablelib/binary": "^1.0.1", "@stablelib/chacha": "^1.0.1", "@stablelib/constant-time": "^1.0.1", "@stablelib/poly1305": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA=="],
|
||||
|
||||
"@stablelib/constant-time": ["@stablelib/constant-time@1.0.1", "", {}, "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg=="],
|
||||
|
||||
"@stablelib/int": ["@stablelib/int@1.0.1", "", {}, "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w=="],
|
||||
|
||||
"@stablelib/poly1305": ["@stablelib/poly1305@1.0.1", "", { "dependencies": { "@stablelib/constant-time": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA=="],
|
||||
|
||||
"@stablelib/wipe": ["@stablelib/wipe@1.0.1", "", {}, "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg=="],
|
||||
|
||||
"@stablelib/xchacha20": ["@stablelib/xchacha20@1.0.1", "", { "dependencies": { "@stablelib/binary": "^1.0.1", "@stablelib/chacha": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-1YkiZnFF4veUwBVhDnDYwo6EHeKzQK4FnLiO7ezCl/zu64uG0bCCAUROJaBkaLH+5BEsO3W7BTXTguMbSLlWSw=="],
|
||||
|
||||
"@stablelib/xchacha20poly1305": ["@stablelib/xchacha20poly1305@1.0.1", "", { "dependencies": { "@stablelib/aead": "^1.0.1", "@stablelib/chacha20poly1305": "^1.0.1", "@stablelib/constant-time": "^1.0.1", "@stablelib/wipe": "^1.0.1", "@stablelib/xchacha20": "^1.0.1" } }, "sha512-B1Abj0sMJ8h3HNmGnJ7vHBrAvxuNka6cJJoZ1ILN7iuacXp7sUYcgOVEOTLWj+rtQMpspY9tXSCRLPmN1mQNWg=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
||||
|
||||
"@types/node": ["@types/node@22.18.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
|
||||
|
||||
"ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="],
|
||||
|
||||
"args-tokenizer": ["args-tokenizer@0.3.0", "", {}, "sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q=="],
|
||||
|
||||
"ast-kit": ["ast-kit@2.1.2", "", { "dependencies": { "@babel/parser": "^7.28.0", "pathe": "^2.0.3" } }, "sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g=="],
|
||||
|
||||
"birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="],
|
||||
|
||||
"bumpp": ["bumpp@10.2.3", "", { "dependencies": { "ansis": "^4.1.0", "args-tokenizer": "^0.3.0", "c12": "^3.2.0", "cac": "^6.7.14", "escalade": "^3.2.0", "jsonc-parser": "^3.3.1", "package-manager-detector": "^1.3.0", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "yaml": "^2.8.1" }, "bin": { "bumpp": "bin/bumpp.mjs" } }, "sha512-nsFBZACxuBVu6yzDSaZZaWpX5hTQ+++9WtYkmO+0Bd3cpSq0Mzvqw5V83n+fOyRj3dYuZRFCQf5Z9NNfZj+Rnw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||
|
||||
"c12": ["c12@3.2.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-ixkEtbYafL56E6HiFuonMm1ZjoKtIo7TH68/uiEq4DAwv9NcUX2nJ95F8TrbMeNjqIkZpruo3ojXQJ+MGG5gcQ=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||
|
||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
|
||||
|
||||
"dts-resolver": ["dts-resolver@2.1.2", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg=="],
|
||||
|
||||
"empathic": ["empathic@1.1.0", "", {}, "sha512-rsPft6CK3eHtrlp9Y5ALBb+hfK+DWnA4WFebbazxjWyx8vSm3rZeoM3z9irsjcqO3PYRzlfv27XIB4tz2DV7RA=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||
|
||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"nypm": ["nypm@0.6.1", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w=="],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"opusscript": ["opusscript@0.1.1", "", {}, "sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
||||
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-beta.9", "", { "dependencies": { "@oxc-project/types": "0.70.0", "@rolldown/pluginutils": "1.0.0-beta.9", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-darwin-arm64": "1.0.0-beta.9", "@rolldown/binding-darwin-x64": "1.0.0-beta.9", "@rolldown/binding-freebsd-x64": "1.0.0-beta.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.9", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.9", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.9", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.9" }, "peerDependencies": { "@oxc-project/runtime": "0.70.0" }, "optionalPeers": ["@oxc-project/runtime"], "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZgZky52n6iF0UainGKjptKGrOG4Con2S5sdc4C4y2Oj25D5PHAY8Y8E5f3M2TSd/zlhQs574JlMeTe3vREczSg=="],
|
||||
|
||||
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.13.14", "", { "dependencies": { "@babel/generator": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/types": "^7.28.1", "ast-kit": "^2.1.1", "birpc": "^2.5.0", "debug": "^4.4.1", "dts-resolver": "^2.1.1", "get-tsconfig": "^4.10.1" }, "peerDependencies": { "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "^2.2.0 || ^3.0.0" }, "optionalPeers": ["@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tsdown": ["tsdown@0.11.13", "", { "dependencies": { "ansis": "^4.0.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "debug": "^4.4.1", "diff": "^8.0.1", "empathic": "^1.1.0", "hookable": "^5.5.3", "rolldown": "1.0.0-beta.9", "rolldown-plugin-dts": "^0.13.3", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.13", "unconfig": "^7.3.2" }, "peerDependencies": { "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.js" } }, "sha512-VSfoNm8MJXFdg7PJ4p2javgjMRiQQHpkP9N3iBBTrmCixcT6YZ9ZtqYMW3NDHczqR0C0Qnur1HMQr1ZfZcmrng=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"unconfig": ["unconfig@7.3.3", "", { "dependencies": { "@quansync/fs": "^0.1.5", "defu": "^6.1.4", "jiti": "^2.5.1", "quansync": "^0.2.11" } }, "sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
||||
}
|
||||
}
|
||||
10
packages/discord/bunfig.toml
Normal file
10
packages/discord/bunfig.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[test]
|
||||
coverage = true
|
||||
coverageReporter = ["text", "lcov"]
|
||||
coveragePathIgnorePatterns = [
|
||||
"fixtures/**",
|
||||
"dist/**"
|
||||
]
|
||||
|
||||
[run]
|
||||
bun = true
|
||||
8
packages/discord/fixtures/commands/test1.command.ts
Normal file
8
packages/discord/fixtures/commands/test1.command.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { CommandHandler } from '@/commands/command-handler.type';
|
||||
|
||||
const handler: CommandHandler<{ name: string; type: 1; description: string }> = {
|
||||
definition: { name: 'test1', type: 1, description: 'Test command 1' },
|
||||
execute: async () => {},
|
||||
};
|
||||
|
||||
export default handler;
|
||||
8
packages/discord/fixtures/commands/test2.command.ts
Normal file
8
packages/discord/fixtures/commands/test2.command.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { CommandHandler } from '@/commands/command-handler.type';
|
||||
|
||||
const handler: CommandHandler<{ name: string; type: 1; description: string }> = {
|
||||
definition: { name: 'test2', type: 1, description: 'Test command 2' },
|
||||
execute: async () => {},
|
||||
};
|
||||
|
||||
export default handler;
|
||||
21
packages/discord/fixtures/jsd/test.ts
Normal file
21
packages/discord/fixtures/jsd/test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as StarKitten from '@star-kitten/discord';
|
||||
import type { ExecutableInteraction } from '@star-kitten/discord';
|
||||
import { createActionRow, createButton, createContainer, createTextDisplay } from '@star-kitten/discord/components';
|
||||
import type { PageContext } from '@star-kitten/discord/pages';
|
||||
import { type Appraisal } from '@star-kitten/eve/third-party/janice.js';
|
||||
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
|
||||
|
||||
export function renderAppraisal(
|
||||
appraisal: Appraisal,
|
||||
pageCtx: PageContext<any>,
|
||||
interaction: ExecutableInteraction,
|
||||
) {
|
||||
const formatter = new Intl.NumberFormat(interaction.locale || 'en-US', {
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
const world = 'world';
|
||||
return (
|
||||
StarKitten.createElement("ActionRow", {}, StarKitten.createElement("Container", {"color":"0x1da57a"}, StarKitten.createElement("TextDisplay", {}, ""+ `Hello ${world}` +""), pageCtx.state.currentPage !== "share" ? StarKitten.createElement("ActionRow", {}, StarKitten.createElement("Button", {"key":"share","disabled":"{!unknown}"}, "Share in Channel")) : undefined))
|
||||
)
|
||||
}
|
||||
29
packages/discord/fixtures/jsd/test.tsx
Normal file
29
packages/discord/fixtures/jsd/test.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import type {} from '@star-kitten/discord/jsx';
|
||||
import { ActionRow, Container, Button, TextDisplay } from '@star-kitten/discord';
|
||||
|
||||
export function renderAppraisal() {
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
const world = 'world';
|
||||
const rand = Math.random() * 1000;
|
||||
const pageCtx = { state: { currentPage: 'home' } };
|
||||
|
||||
let jsx = (
|
||||
<ActionRow>
|
||||
<Container color="0x1da57a">
|
||||
<TextDisplay content={`Hello ${world}`} />
|
||||
{pageCtx.state.currentPage !== 'share' ?
|
||||
<ActionRow>
|
||||
<Button customId="share" label="Share in Channel" disabled={rand < 500} />
|
||||
</ActionRow>
|
||||
: undefined}
|
||||
</Container>
|
||||
</ActionRow>
|
||||
);
|
||||
|
||||
console.log(jsx);
|
||||
}
|
||||
|
||||
renderAppraisal();
|
||||
69
packages/discord/index.d.ts
vendored
Normal file
69
packages/discord/index.d.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
type ActionRow,
|
||||
type Button,
|
||||
type ChannelSelectMenu,
|
||||
type GuildChannelTypes,
|
||||
type MentionableSelectMenu,
|
||||
type PartialEmoji,
|
||||
type RoleSelectMenu,
|
||||
type StringSelectMenu,
|
||||
type TextInput,
|
||||
type UserSelectMenu,
|
||||
type LabelComponent,
|
||||
type ContainerComponent,
|
||||
type TextDisplayComponent,
|
||||
type SectionComponent,
|
||||
type MediaGalleryComponent,
|
||||
type SeparatorComponent,
|
||||
type FileComponent,
|
||||
type InteractionButton,
|
||||
type URLButton,
|
||||
type PremiumButton,
|
||||
type ThumbnailComponent,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
declare namespace JSX {
|
||||
type Component =
|
||||
| ActionRow
|
||||
| Button
|
||||
| StringSelectMenu
|
||||
| UserSelectMenu
|
||||
| RoleSelectMenu
|
||||
| MentionableSelectMenu
|
||||
| ChannelSelectMenu
|
||||
| TextInput
|
||||
| LabelComponent
|
||||
| ContainerComponent
|
||||
| {
|
||||
type: 10;
|
||||
content: string;
|
||||
}
|
||||
| SectionComponent
|
||||
| MediaGalleryComponent
|
||||
| SeparatorComponent
|
||||
| FileComponent
|
||||
| InteractionButton
|
||||
| URLButton
|
||||
| PremiumButton
|
||||
| ThumbnailComponent;
|
||||
|
||||
type Element = Component | Promise<Component>;
|
||||
|
||||
interface ElementClass {
|
||||
render: any;
|
||||
}
|
||||
|
||||
interface ElementAttributesProperty {
|
||||
props: {};
|
||||
}
|
||||
|
||||
interface IntrinsicElements {
|
||||
// Allow any element, but prefer known elements
|
||||
[elemName: string]: any;
|
||||
// Known elements
|
||||
ActionRow: { children: any | any[] };
|
||||
Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean };
|
||||
Container: { accent?: number; spoiler?: boolean; children: any | any[] };
|
||||
TextDisplay: { content: string };
|
||||
}
|
||||
}
|
||||
58
packages/discord/package.json
Normal file
58
packages/discord/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@star-kitten/discord",
|
||||
"version": "0.0.0",
|
||||
"description": "Star Kitten Discord library",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/author/library#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/author/library/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/author/library.git"
|
||||
},
|
||||
"author": "Author Name <author.name@mail.com>",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./commands": "./dist/commands/index.js",
|
||||
"./components": "./dist/components/index.js",
|
||||
"./pages": "./dist/pages/index.js",
|
||||
"./common": "./dist/common/index.js",
|
||||
"./package.json": "./package.json",
|
||||
"./jsx": "./src/jsx/jsx.ts",
|
||||
"./jsx-runtime": "./dist/jsx/index.js",
|
||||
"./jsx-dev-runtime": "./dist/jsx/index.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"release": "bumpp && npm publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.21",
|
||||
"@types/node": "^22.15.17",
|
||||
"bumpp": "^10.1.0",
|
||||
"tsdown": "^0.11.9",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
|
||||
"@star-kitten/util": "workspace:^0.0.0",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"html-dom-parser": "^5.1.1",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
}
|
||||
14
packages/discord/src/commands/command-context.type.ts
Normal file
14
packages/discord/src/commands/command-context.type.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Cache } from '@core/cache.type';
|
||||
import type { KVStore } from '@core/kv-store.type.ts';
|
||||
import type { Client } from '@projectdysnomia/dysnomia';
|
||||
import type { CommandState } from './command-state';
|
||||
|
||||
export interface PartialContext<T = any> {
|
||||
client: Client;
|
||||
cache: Cache;
|
||||
kv: KVStore;
|
||||
id?: string; // unique id for this command instance
|
||||
state?: CommandState<T>; // state associated with this command instance
|
||||
}
|
||||
|
||||
export type CommandContext<T = any> = Required<PartialContext<T>>;
|
||||
32
packages/discord/src/commands/command-handler.ts
Normal file
32
packages/discord/src/commands/command-handler.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
AutocompleteInteraction,
|
||||
CommandInteraction,
|
||||
ComponentInteraction,
|
||||
Constants,
|
||||
ModalSubmitInteraction,
|
||||
type ApplicationCommandOptionAutocomplete,
|
||||
type ApplicationCommandOptions,
|
||||
type ApplicationCommandStructure,
|
||||
type ChatInputApplicationCommandStructure,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
import type { CommandContext, PartialContext } from './command-context.type';
|
||||
|
||||
export interface CommandHandler<T extends ApplicationCommandStructure> {
|
||||
definition: T;
|
||||
execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise<void>;
|
||||
}
|
||||
|
||||
export type ExecutableInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction | AutocompleteInteraction;
|
||||
|
||||
export type ChatCommandDefinition = Omit<ChatInputApplicationCommandStructure, 'type'>;
|
||||
export function createChatCommand(
|
||||
definition: ChatCommandDefinition,
|
||||
execute: (interaction: CommandInteraction, ctx: CommandContext) => Promise<void>,
|
||||
): CommandHandler<ChatInputApplicationCommandStructure> {
|
||||
const def = definition as ChatInputApplicationCommandStructure;
|
||||
def.type = 1; // CHAT_INPUT
|
||||
return {
|
||||
definition: def,
|
||||
execute,
|
||||
};
|
||||
}
|
||||
45
packages/discord/src/commands/command-helpers.ts
Normal file
45
packages/discord/src/commands/command-helpers.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
Interaction,
|
||||
CommandInteraction,
|
||||
Constants,
|
||||
ModalSubmitInteraction,
|
||||
ComponentInteraction,
|
||||
AutocompleteInteraction,
|
||||
PingInteraction,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
import type { ExecutableInteraction } from './command-handler';
|
||||
|
||||
export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND;
|
||||
}
|
||||
|
||||
export function isModalSubmit(interaction: Interaction): interaction is ModalSubmitInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.MODAL_SUBMIT;
|
||||
}
|
||||
|
||||
export function isMessageComponent(interaction: Interaction): interaction is ComponentInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.MESSAGE_COMPONENT;
|
||||
}
|
||||
|
||||
export function isAutocomplete(interaction: Interaction): interaction is AutocompleteInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE;
|
||||
}
|
||||
|
||||
export function isPing(interaction: Interaction): interaction is PingInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.PING;
|
||||
}
|
||||
|
||||
export function commandHasName(interaction: Interaction, name: string): boolean {
|
||||
return isApplicationCommand(interaction) && interaction.data.name === name;
|
||||
}
|
||||
|
||||
export function commandHasIdPrefix(interaction: Interaction, prefix: string): boolean {
|
||||
return (isModalSubmit(interaction) || isMessageComponent(interaction)) && interaction.data.custom_id.startsWith(prefix);
|
||||
}
|
||||
|
||||
export function getCommandName(interaction: ExecutableInteraction): string | undefined {
|
||||
if (isApplicationCommand(interaction) || isAutocomplete(interaction)) {
|
||||
return interaction.data.name;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
63
packages/discord/src/commands/command-injection.ts
Normal file
63
packages/discord/src/commands/command-injection.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { type InteractionModalContent, type Component } from '@projectdysnomia/dysnomia';
|
||||
import type { CommandContext, PartialContext } from './command-context.type';
|
||||
import { isApplicationCommand, isMessageComponent } from './command-helpers';
|
||||
import type { ExecutableInteraction } from './command-handler';
|
||||
|
||||
export function injectInteraction(interaction: ExecutableInteraction, ctx: PartialContext): [ExecutableInteraction, CommandContext] {
|
||||
// Wrap the interaction methods to inject command tracking ids into all custom_ids for modals and components.
|
||||
if (ctx.state.name && (isApplicationCommand(interaction) || isMessageComponent(interaction))) {
|
||||
const _originalCreateModal = interaction.createModal.bind(interaction);
|
||||
interaction.createModal = (content: InteractionModalContent) => {
|
||||
validateCustomIdLength(content.custom_id);
|
||||
content.custom_id = `${content.custom_id}_${ctx.state.id}`;
|
||||
return _originalCreateModal(content);
|
||||
};
|
||||
|
||||
const _originalCreateMessage = interaction.createMessage.bind(interaction);
|
||||
interaction.createMessage = (content) => {
|
||||
if (typeof content === 'string') return _originalCreateMessage(content);
|
||||
if (content.components) {
|
||||
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||
}
|
||||
return _originalCreateMessage(content);
|
||||
};
|
||||
|
||||
const _originalEditMessage = interaction.editMessage.bind(interaction);
|
||||
interaction.editMessage = (messageID, content) => {
|
||||
if (typeof content === 'string') return _originalEditMessage(messageID, content);
|
||||
if (content.components) {
|
||||
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||
}
|
||||
return _originalEditMessage(messageID, content);
|
||||
};
|
||||
|
||||
const _originalCreateFollowup = interaction.createFollowup.bind(interaction);
|
||||
interaction.createFollowup = (content) => {
|
||||
if (typeof content === 'string') return _originalCreateFollowup(content);
|
||||
if (content.components) {
|
||||
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||
}
|
||||
return _originalCreateFollowup(content);
|
||||
};
|
||||
}
|
||||
return [interaction, ctx as CommandContext];
|
||||
}
|
||||
|
||||
function validateCustomIdLength(customId: string) {
|
||||
if (customId.length > 80) {
|
||||
throw new Error(`Custom ID too long: ${customId.length} characters (max 80) with this framework. Consider using shorter IDs.`);
|
||||
}
|
||||
}
|
||||
|
||||
function addCommandIdToComponentCustomIds(components: Component[], commandId: string) {
|
||||
components.forEach((component) => {
|
||||
if (!component) return;
|
||||
if ('custom_id' in component) {
|
||||
validateCustomIdLength(component.custom_id as string);
|
||||
component.custom_id = `${component.custom_id}_${commandId}`;
|
||||
}
|
||||
if ('components' in component && Array.isArray(component.components)) {
|
||||
addCommandIdToComponentCustomIds(component.components, commandId);
|
||||
}
|
||||
});
|
||||
}
|
||||
56
packages/discord/src/commands/command-state.ts
Normal file
56
packages/discord/src/commands/command-state.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { createReactiveState } from '@star-kitten/util/reactive-state.js';
|
||||
import type { PartialContext } from './command-context.type';
|
||||
import { isApplicationCommand, isAutocomplete } from './command-helpers';
|
||||
import type { ExecutableInteraction } from './command-handler';
|
||||
|
||||
export interface CommandState<T = any> {
|
||||
id: string; // unique id for this command instance
|
||||
name: string; // command name
|
||||
data: T; // internal data storage
|
||||
}
|
||||
|
||||
export async function getCommandState<T>(interaction: ExecutableInteraction, ctx: PartialContext): Promise<CommandState<T>> {
|
||||
const id = instanceIdFromInteraction(interaction);
|
||||
|
||||
let state: CommandState<T>;
|
||||
|
||||
// get state from kv store if possible
|
||||
if (ctx.kv.has(`command-state:${id}`)) {
|
||||
state = await ctx.kv.get<CommandState<T>>(`command-state:${id}`);
|
||||
}
|
||||
if (!state) {
|
||||
state = { id: id, name: '', data: {} as T };
|
||||
}
|
||||
const [reactiveState, subscribe] = createReactiveState(state);
|
||||
subscribe(async (newState) => {
|
||||
if (ctx.kv) {
|
||||
await ctx.kv.set(`command-state:${id}`, newState);
|
||||
}
|
||||
});
|
||||
ctx.state = reactiveState;
|
||||
return reactiveState;
|
||||
}
|
||||
|
||||
function instanceIdFromInteraction(interaction: ExecutableInteraction) {
|
||||
if (isAutocomplete(interaction)) {
|
||||
// autocomplete should not be stateful, they get no id
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isApplicationCommand(interaction)) {
|
||||
// for application commands, we create a new instance id
|
||||
const instance_id = crypto.randomUUID();
|
||||
return instance_id;
|
||||
}
|
||||
|
||||
const interact = interaction;
|
||||
const customId: string = interact.data.custom_id;
|
||||
const commandId = customId.split('_').pop();
|
||||
interaction;
|
||||
// command id should be a uuid
|
||||
if (commandId && /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(commandId)) {
|
||||
return commandId;
|
||||
}
|
||||
console.error(`Invalid command id extracted from interaction: ${customId}`);
|
||||
return '';
|
||||
}
|
||||
59
packages/discord/src/commands/handle-commands.test.ts
Normal file
59
packages/discord/src/commands/handle-commands.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { expect, test, mock, beforeEach, afterEach } from 'bun:test';
|
||||
import { handleCommands } from './handle-commands';
|
||||
import { CommandInteraction, Constants, ModalSubmitInteraction, type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||
import type { CommandHandler } from '../../dist';
|
||||
|
||||
let commands: Record<string, CommandHandler<ApplicationCommandStructure>>;
|
||||
|
||||
beforeEach(() => {
|
||||
commands = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
commands = {};
|
||||
});
|
||||
|
||||
mock.module('./command-helpers', () => ({
|
||||
getCommandName: () => 'testCommand',
|
||||
}));
|
||||
|
||||
test('handleCommands executes command when interaction is CommandInteraction and command exists', async () => {
|
||||
const mockExecute = mock(() => Promise.resolve());
|
||||
const mockCommand = { definition: { name: 'testCommand' } as any, execute: mockExecute };
|
||||
commands['testCommand'] = mockCommand;
|
||||
|
||||
const mockInteraction = {
|
||||
type: Constants.InteractionTypes.APPLICATION_COMMAND,
|
||||
data: { name: 'testCommand' },
|
||||
} as any;
|
||||
Object.setPrototypeOf(mockInteraction, CommandInteraction.prototype);
|
||||
|
||||
handleCommands(mockInteraction, commands, {} as any);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledWith(mockInteraction, expect.any(Object));
|
||||
});
|
||||
|
||||
test('handleCommands executes command when interaction is CommandInteraction and command exists', async () => {
|
||||
const mockExecute = mock(() => Promise.resolve());
|
||||
const mockCommand = { definition: { name: 'testCommand' } as any, execute: mockExecute };
|
||||
commands['testCommand'] = mockCommand;
|
||||
|
||||
const mockInteraction = {
|
||||
type: Constants.InteractionTypes.MODAL_SUBMIT,
|
||||
data: { name: 'testCommand' },
|
||||
} as any;
|
||||
Object.setPrototypeOf(mockInteraction, ModalSubmitInteraction.prototype);
|
||||
|
||||
handleCommands(mockInteraction, commands, {} as any);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledWith(mockInteraction, expect.any(Object));
|
||||
});
|
||||
|
||||
test('handleCommands does nothing when interaction not a CommandInteraction, ModalSubmitInteraction, MessageComponentInteraction, or AutoCompleteInteraction', () => {
|
||||
const mockInteraction = {
|
||||
instanceof: (cls: any) => false,
|
||||
} as any;
|
||||
|
||||
// Should not throw or do anything
|
||||
expect(() => handleCommands(mockInteraction, commands, {} as any)).not.toThrow();
|
||||
});
|
||||
73
packages/discord/src/commands/handle-commands.ts
Normal file
73
packages/discord/src/commands/handle-commands.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||
import { getCommandName, isApplicationCommand, isAutocomplete, isMessageComponent, isModalSubmit } from './command-helpers';
|
||||
import type { PartialContext } from './command-context.type';
|
||||
import type { CommandHandler, ExecutableInteraction } from './command-handler';
|
||||
import { injectInteraction } from './command-injection';
|
||||
import { getCommandState } from './command-state';
|
||||
|
||||
export async function handleCommands(
|
||||
interaction: ExecutableInteraction,
|
||||
commands: Record<string, CommandHandler<ApplicationCommandStructure>>,
|
||||
ctx: PartialContext,
|
||||
) {
|
||||
ctx.state = await getCommandState(interaction, ctx);
|
||||
if (!ctx.state.name) {
|
||||
ctx.state.name = getCommandName(interaction);
|
||||
}
|
||||
|
||||
if (isAutocomplete(interaction) && ctx.state.name) {
|
||||
const acCommand = commands[ctx.state.name];
|
||||
return acCommand.execute(interaction, ctx as any);
|
||||
}
|
||||
|
||||
if (!ctx.state.id) {
|
||||
console.error(`No command ID found for interaction ${interaction.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const command = commands[ctx.state.name || ''];
|
||||
if (!command) {
|
||||
console.warn(`No command found for interaction: ${JSON.stringify(interaction, undefined, 2)}`);
|
||||
return;
|
||||
}
|
||||
cleanInteractionCustomIds(interaction, ctx.state.id);
|
||||
const [injectedInteraction, fullContext] = await injectInteraction(interaction, ctx);
|
||||
return command.execute(injectedInteraction, fullContext);
|
||||
}
|
||||
|
||||
export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) {
|
||||
ctx.client.on('interactionCreate', async (interaction) => {
|
||||
if (isApplicationCommand(interaction) || isModalSubmit(interaction) || isMessageComponent(interaction) || isAutocomplete(interaction)) {
|
||||
handleCommands(interaction, commands, ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cleanInteractionCustomIds(interaction: ExecutableInteraction, id: string) {
|
||||
if ('components' in interaction && Array.isArray(interaction.components) && id) {
|
||||
removeCommandIdFromComponentCustomIds(interaction.components, id);
|
||||
}
|
||||
if ('data' in interaction && id) {
|
||||
if ('custom_id' in interaction.data && typeof interaction.data.custom_id === 'string') {
|
||||
interaction.data.custom_id = interaction.data.custom_id.replace(`_${id}`, '');
|
||||
}
|
||||
if ('components' in interaction.data && Array.isArray(interaction.data.components)) {
|
||||
removeCommandIdFromComponentCustomIds(interaction.data.components as any, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeCommandIdFromComponentCustomIds(components: { custom_id?: string; components?: any[] }[], commandId: string) {
|
||||
components.forEach((component) => {
|
||||
if ('custom_id' in component) {
|
||||
component.custom_id = component.custom_id.replace(`_${commandId}`, '');
|
||||
}
|
||||
if ('components' in component && Array.isArray(component.components)) {
|
||||
removeCommandIdFromComponentCustomIds(component.components, commandId);
|
||||
}
|
||||
|
||||
if ('component' in component && 'custom_id' in (component as any).component && Array.isArray(component.components)) {
|
||||
(component.component as any).custom_id = (component.component as any).custom_id.replace(`_${commandId}`, '');
|
||||
}
|
||||
});
|
||||
}
|
||||
19
packages/discord/src/commands/import-commands.test.ts
Normal file
19
packages/discord/src/commands/import-commands.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { expect, test, mock } from 'bun:test';
|
||||
import { importCommands } from './import-commands';
|
||||
import path from 'node:path';
|
||||
|
||||
test('importCommands imports commands from files matching pattern', async () => {
|
||||
const commands = await importCommands('**/*.command.{js,ts}', path.join(__dirname, '../../fixtures'));
|
||||
|
||||
expect(commands).toHaveProperty('test1');
|
||||
expect(commands).toHaveProperty('test2');
|
||||
expect(commands.test1.definition.name).toBe('test1');
|
||||
expect(commands.test2.definition.name).toBe('test2');
|
||||
});
|
||||
|
||||
test('importCommands uses default pattern and baseDir', async () => {
|
||||
const commands = await importCommands();
|
||||
|
||||
// Since there are no command files in src, it should be empty
|
||||
expect(commands).toEqual({});
|
||||
});
|
||||
19
packages/discord/src/commands/import-commands.ts
Normal file
19
packages/discord/src/commands/import-commands.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Glob } from 'bun';
|
||||
import { join } from 'node:path';
|
||||
import type { CommandHandler } from './command-handler';
|
||||
import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||
|
||||
export async function importCommands(
|
||||
pattern: string = '**/*.command.{js,ts}',
|
||||
baseDir: string = join(process.cwd(), 'src'),
|
||||
commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {},
|
||||
): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> {
|
||||
const glob = new Glob(pattern);
|
||||
|
||||
for await (const file of glob.scan({ cwd: baseDir, absolute: true })) {
|
||||
const command = (await import(file)).default as CommandHandler<ApplicationCommandStructure>;
|
||||
commandRegistry[command.definition.name] = command;
|
||||
}
|
||||
|
||||
return commandRegistry;
|
||||
}
|
||||
8
packages/discord/src/commands/index.ts
Normal file
8
packages/discord/src/commands/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './command-handler';
|
||||
export * from './import-commands';
|
||||
export * from './handle-commands';
|
||||
export * from './command-helpers';
|
||||
export * from './register-commands';
|
||||
export * from './command-context.type';
|
||||
export * from './command-state';
|
||||
export * from './option-builders';
|
||||
80
packages/discord/src/commands/option-builders.ts
Normal file
80
packages/discord/src/commands/option-builders.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
Constants,
|
||||
type ApplicationCommandOptions,
|
||||
type ApplicationCommandOptionsBoolean,
|
||||
type ApplicationCommandOptionsInteger,
|
||||
type ApplicationCommandOptionsMentionable,
|
||||
type ApplicationCommandOptionsNumber,
|
||||
type ApplicationCommandOptionsRole,
|
||||
type ApplicationCommandOptionsString,
|
||||
type ApplicationCommandOptionsSubCommand,
|
||||
type ApplicationCommandOptionsSubCommandGroup,
|
||||
type ApplicationCommandOptionsUser,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
export type StringOptionDefinition = Omit<ApplicationCommandOptionsString, 'type'> & { autocomplete?: boolean };
|
||||
export function stringOption(options: StringOptionDefinition): ApplicationCommandOptionsString {
|
||||
const def = options as ApplicationCommandOptionsString;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.STRING;
|
||||
return def;
|
||||
}
|
||||
export type IntegerOptionDefinition = Omit<ApplicationCommandOptionsInteger, 'type'> & { autocomplete?: boolean };
|
||||
export function integerOption(options: IntegerOptionDefinition): ApplicationCommandOptionsInteger {
|
||||
const def = options as ApplicationCommandOptionsInteger;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.INTEGER;
|
||||
return def;
|
||||
}
|
||||
export type BooleanOptionDefinition = Omit<ApplicationCommandOptionsBoolean, 'type'>;
|
||||
export function booleanOption(options: BooleanOptionDefinition): ApplicationCommandOptionsBoolean {
|
||||
const def = options as ApplicationCommandOptionsBoolean;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.BOOLEAN;
|
||||
return def;
|
||||
}
|
||||
export type UserOptionDefinition = Omit<ApplicationCommandOptionsUser, 'type'> & { autocomplete?: boolean };
|
||||
export function userOption(options: UserOptionDefinition): ApplicationCommandOptionsUser {
|
||||
const def = options as ApplicationCommandOptionsUser;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.USER;
|
||||
return def;
|
||||
}
|
||||
export type ChannelOptionDefinition = Omit<ApplicationCommandOptions, 'type'> & { autocomplete?: boolean };
|
||||
export function channelOption(options: ChannelOptionDefinition): ApplicationCommandOptions {
|
||||
const def = options as ApplicationCommandOptions;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.CHANNEL;
|
||||
return def;
|
||||
}
|
||||
export type RoleOptionDefinition = Omit<ApplicationCommandOptionsRole, 'type'> & { autocomplete?: boolean };
|
||||
export function roleOption(options: RoleOptionDefinition): ApplicationCommandOptionsRole {
|
||||
const def = options as ApplicationCommandOptionsRole;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.ROLE;
|
||||
return def;
|
||||
}
|
||||
export type MentionableOptionDefinition = Omit<ApplicationCommandOptionsMentionable, 'type'>;
|
||||
export function mentionableOption(options: MentionableOptionDefinition): ApplicationCommandOptionsMentionable {
|
||||
const def = options as ApplicationCommandOptionsMentionable;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.MENTIONABLE;
|
||||
return def;
|
||||
}
|
||||
export type NumberOptionDefinition = Omit<ApplicationCommandOptionsNumber, 'type'> & { autocomplete?: boolean };
|
||||
export function numberOption(options: NumberOptionDefinition): ApplicationCommandOptionsNumber {
|
||||
const def = options as ApplicationCommandOptionsNumber;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.NUMBER;
|
||||
return def;
|
||||
}
|
||||
export type AttachmentOptionDefinition = Omit<ApplicationCommandOptions, 'type'>;
|
||||
export function attachmentOption(options: AttachmentOptionDefinition): ApplicationCommandOptions {
|
||||
const def = options as ApplicationCommandOptions;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.ATTACHMENT;
|
||||
return def;
|
||||
}
|
||||
export type SubCommandOptionDefinition = Omit<ApplicationCommandOptionsSubCommand, 'type'>;
|
||||
export function subCommandOption(options: SubCommandOptionDefinition): ApplicationCommandOptionsSubCommand {
|
||||
const def = options as ApplicationCommandOptionsSubCommand;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND;
|
||||
return def;
|
||||
}
|
||||
export type SubCommandGroupOptionDefinition = Omit<ApplicationCommandOptionsSubCommandGroup, 'type'>;
|
||||
export function subCommandGroupOption(options: SubCommandGroupOptionDefinition): ApplicationCommandOptionsSubCommandGroup {
|
||||
const def = options as ApplicationCommandOptionsSubCommandGroup;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND_GROUP;
|
||||
return def;
|
||||
}
|
||||
11
packages/discord/src/commands/register-commands.ts
Normal file
11
packages/discord/src/commands/register-commands.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ApplicationCommandStructure, Client } from '@projectdysnomia/dysnomia';
|
||||
|
||||
export async function registerCommands(client: Client, commands: ApplicationCommandStructure[]) {
|
||||
if (!client) throw new Error('Client not initialized');
|
||||
if (!(await client.getCommands()).length || process.env.RESET_COMMANDS === 'true' || process.env.NODE_ENV === 'development') {
|
||||
console.debug('Registering commands...');
|
||||
const response = await client.bulkEditCommands(commands);
|
||||
console.debug(`Registered ${response.length} commands.`);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
1
packages/discord/src/common/index.ts
Normal file
1
packages/discord/src/common/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './text';
|
||||
2
packages/discord/src/common/text.ts
Normal file
2
packages/discord/src/common/text.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const WHITE_SPACE = ' '; // non-breaking space
|
||||
export const BREAKING_WHITE_SPACE = '\u200B';
|
||||
314
packages/discord/src/components/builders.ts
Normal file
314
packages/discord/src/components/builders.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import {
|
||||
Constants,
|
||||
type ActionRow,
|
||||
type Button,
|
||||
type ChannelSelectMenu,
|
||||
type GuildChannelTypes,
|
||||
type MentionableSelectMenu,
|
||||
type PartialEmoji,
|
||||
type RoleSelectMenu,
|
||||
type StringSelectMenu,
|
||||
type TextInput,
|
||||
type UserSelectMenu,
|
||||
type LabelComponent,
|
||||
type ContainerComponent,
|
||||
type TextDisplayComponent,
|
||||
type SectionComponent,
|
||||
type MediaGalleryComponent,
|
||||
type SeparatorComponent,
|
||||
type FileComponent,
|
||||
type InteractionButton,
|
||||
type URLButton,
|
||||
type PremiumButton,
|
||||
type ThumbnailComponent,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
export type ActionRowItem = Button | StringSelectMenu | UserSelectMenu | RoleSelectMenu | MentionableSelectMenu | ChannelSelectMenu;
|
||||
export const createActionRow = (...components: ActionRowItem[]): ActionRow => ({
|
||||
type: Constants.ComponentTypes.ACTION_ROW,
|
||||
components,
|
||||
});
|
||||
|
||||
export enum ButtonStyle {
|
||||
PRIMARY = 1,
|
||||
SECONDARY = 2,
|
||||
SUCCESS = 3,
|
||||
DANGER = 4,
|
||||
}
|
||||
|
||||
export interface ButtonOptions {
|
||||
style?: ButtonStyle;
|
||||
emoji?: PartialEmoji;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const createButton = (label: string, custom_id: string, options?: ButtonOptions): InteractionButton => ({
|
||||
type: Constants.ComponentTypes.BUTTON,
|
||||
style: options?.style ?? Constants.ButtonStyles.PRIMARY,
|
||||
label,
|
||||
custom_id,
|
||||
...options,
|
||||
});
|
||||
|
||||
export interface URLButtonOptions {
|
||||
emoji?: PartialEmoji;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const createURLButton = (label: string, url: string, options?: URLButtonOptions): URLButton => ({
|
||||
type: Constants.ComponentTypes.BUTTON,
|
||||
style: Constants.ButtonStyles.LINK,
|
||||
label,
|
||||
url,
|
||||
...options,
|
||||
});
|
||||
|
||||
export interface PremiumButtonOptions {
|
||||
emoji?: PartialEmoji;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const createPremiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({
|
||||
type: Constants.ComponentTypes.BUTTON,
|
||||
style: Constants.ButtonStyles.PREMIUM,
|
||||
sku_id,
|
||||
...options,
|
||||
});
|
||||
|
||||
export interface StringSelectOpts {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
required?: boolean; // Note: not actually a property of StringSelectMenu, but useful for modals
|
||||
}
|
||||
|
||||
export interface StringSelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
emoji?: {
|
||||
name?: string;
|
||||
id?: string;
|
||||
animated?: boolean;
|
||||
};
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export const createStringSelect = (
|
||||
custom_id: string,
|
||||
selectOpts: StringSelectOpts,
|
||||
...options: StringSelectOption[]
|
||||
): StringSelectMenu => ({
|
||||
type: Constants.ComponentTypes.STRING_SELECT,
|
||||
custom_id,
|
||||
options,
|
||||
placeholder: selectOpts.placeholder ?? '',
|
||||
min_values: selectOpts.min_values ?? 1,
|
||||
max_values: selectOpts.max_values ?? 1,
|
||||
disabled: selectOpts.disabled ?? false,
|
||||
required: selectOpts.required ?? false, // Note: not actually a property of StringSelectMenu, but useful for modals
|
||||
});
|
||||
|
||||
export interface TextInputOptions {
|
||||
isParagraph?: boolean;
|
||||
label?: string;
|
||||
min_length?: number;
|
||||
max_length?: number;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const createTextInput = (custom_id: string, options?: TextInputOptions): TextInput => ({
|
||||
type: Constants.ComponentTypes.TEXT_INPUT,
|
||||
custom_id,
|
||||
style: options.isParagraph ? Constants.TextInputStyles.PARAGRAPH : Constants.TextInputStyles.SHORT,
|
||||
label: options?.label ?? '',
|
||||
min_length: options?.min_length ?? 0,
|
||||
max_length: options?.max_length ?? 4000,
|
||||
required: options?.required ?? false,
|
||||
value: options?.value ?? '',
|
||||
placeholder: options?.placeholder ?? '',
|
||||
});
|
||||
|
||||
export interface UserSelectOptions {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
default_values?: Array<{ id: string; type: 'user' }>;
|
||||
}
|
||||
|
||||
export const createUserSelect = (custom_id: string, options?: UserSelectOptions): UserSelectMenu => ({
|
||||
type: Constants.ComponentTypes.USER_SELECT,
|
||||
custom_id,
|
||||
placeholder: options?.placeholder ?? '',
|
||||
min_values: options?.min_values ?? 1,
|
||||
max_values: options?.max_values ?? 1,
|
||||
disabled: options?.disabled ?? false,
|
||||
default_values: options?.default_values ?? [],
|
||||
});
|
||||
|
||||
export interface RoleSelectOptions {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
default_values?: Array<{ id: string; type: 'role' }>;
|
||||
}
|
||||
|
||||
export const createRoleSelect = (custom_id: string, options?: RoleSelectOptions): RoleSelectMenu => ({
|
||||
type: Constants.ComponentTypes.ROLE_SELECT,
|
||||
custom_id,
|
||||
placeholder: options?.placeholder ?? '',
|
||||
min_values: options?.min_values ?? 1,
|
||||
max_values: options?.max_values ?? 1,
|
||||
disabled: options?.disabled ?? false,
|
||||
default_values: options?.default_values ?? [],
|
||||
});
|
||||
|
||||
export interface MentionableSelectOptions {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
default_values?: Array<{ id: string; type: 'user' | 'role' }>;
|
||||
}
|
||||
|
||||
export const createMentionableSelect = (custom_id: string, options?: MentionableSelectOptions): MentionableSelectMenu => ({
|
||||
type: Constants.ComponentTypes.MENTIONABLE_SELECT,
|
||||
custom_id,
|
||||
placeholder: options?.placeholder ?? '',
|
||||
min_values: options?.min_values ?? 1,
|
||||
max_values: options?.max_values ?? 1,
|
||||
disabled: options?.disabled ?? false,
|
||||
default_values: options?.default_values ?? [],
|
||||
});
|
||||
|
||||
export interface ChannelSelectOptions {
|
||||
channel_types?: GuildChannelTypes[];
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
default_values?: Array<{ id: string; type: 'channel' }>;
|
||||
}
|
||||
|
||||
export const createChannelSelect = (custom_id: string, options?: ChannelSelectOptions): ChannelSelectMenu => ({
|
||||
type: Constants.ComponentTypes.CHANNEL_SELECT,
|
||||
custom_id,
|
||||
channel_types: options?.channel_types ?? [],
|
||||
placeholder: options?.placeholder ?? '',
|
||||
min_values: options?.min_values ?? 1,
|
||||
max_values: options?.max_values ?? 1,
|
||||
disabled: options?.disabled ?? false,
|
||||
default_values: options?.default_values ?? [],
|
||||
});
|
||||
|
||||
export interface SectionOptions {
|
||||
components: Array<TextDisplayComponent>;
|
||||
accessory: Button | ThumbnailComponent;
|
||||
}
|
||||
|
||||
export const createSection = (accessory: Button | ThumbnailComponent, ...components: Array<TextDisplayComponent>): SectionComponent => ({
|
||||
type: Constants.ComponentTypes.SECTION,
|
||||
accessory,
|
||||
components,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a text display component where the text will be displayed similar to a message: supports markdown
|
||||
* @param content The text content to display.
|
||||
* @returns The created text display component.
|
||||
*/
|
||||
export const createTextDisplay = (content: string) => ({
|
||||
type: Constants.ComponentTypes.TEXT_DISPLAY,
|
||||
content,
|
||||
});
|
||||
|
||||
export interface ThumbnailOptions {
|
||||
media: {
|
||||
url: string; // Supports arbitrary urls and attachment://<filename> references
|
||||
};
|
||||
description?: string;
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
export const createThumbnail = (url: string, description?: string, spoiler?: boolean): ThumbnailComponent => ({
|
||||
type: Constants.ComponentTypes.THUMBNAIL,
|
||||
media: {
|
||||
url,
|
||||
},
|
||||
description,
|
||||
spoiler,
|
||||
});
|
||||
|
||||
export interface MediaItem {
|
||||
url: string; // Supports arbitrary urls and attachment://<filename> references
|
||||
description?: string;
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
export const createMediaGallery = (...items: MediaItem[]): MediaGalleryComponent => ({
|
||||
type: Constants.ComponentTypes.MEDIA_GALLERY,
|
||||
items: items.map((item) => ({
|
||||
type: Constants.ComponentTypes.FILE,
|
||||
media: { url: item.url },
|
||||
description: item.description,
|
||||
spoiler: item.spoiler,
|
||||
})),
|
||||
});
|
||||
|
||||
export interface FileOptions {
|
||||
url: string; // Supports only attachment://<filename> references
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
export const createFile = (url: string, spoiler?: boolean): FileComponent => ({
|
||||
type: Constants.ComponentTypes.FILE,
|
||||
file: {
|
||||
url,
|
||||
},
|
||||
spoiler,
|
||||
});
|
||||
|
||||
export enum Padding {
|
||||
SMALL = 1,
|
||||
LARGE = 2,
|
||||
}
|
||||
|
||||
export interface SeparatorOptions {
|
||||
divider?: boolean;
|
||||
spacing?: Padding;
|
||||
}
|
||||
export const createSeparator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({
|
||||
type: Constants.ComponentTypes.SEPARATOR,
|
||||
divider,
|
||||
spacing: spacing ?? Padding.SMALL,
|
||||
});
|
||||
|
||||
export interface ContainerOptions {
|
||||
accent_color?: number;
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
export type ContainerItems =
|
||||
| ActionRow
|
||||
| TextDisplayComponent
|
||||
| SectionComponent
|
||||
| MediaGalleryComponent
|
||||
| SeparatorComponent
|
||||
| FileComponent;
|
||||
|
||||
export const createContainer = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({
|
||||
type: Constants.ComponentTypes.CONTAINER,
|
||||
...options,
|
||||
components,
|
||||
});
|
||||
|
||||
export const createModalLabel = (label: string, component: TextInput | StringSelectMenu): LabelComponent => ({
|
||||
type: Constants.ComponentTypes.LABEL,
|
||||
label,
|
||||
component,
|
||||
});
|
||||
23
packages/discord/src/components/helpers.ts
Normal file
23
packages/discord/src/components/helpers.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
Constants,
|
||||
type ComponentBase,
|
||||
type ModalSubmitInteractionDataLabelComponent,
|
||||
type ModalSubmitInteractionDataStringSelectComponent,
|
||||
type ModalSubmitInteractionDataTextInputComponent,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
export function isModalLabel(component: ComponentBase): component is ModalSubmitInteractionDataLabelComponent {
|
||||
return component.type === Constants.ComponentTypes.LABEL;
|
||||
}
|
||||
|
||||
export function isModalTextInput(component: ComponentBase): component is ModalSubmitInteractionDataTextInputComponent {
|
||||
return component.type === Constants.ComponentTypes.TEXT_INPUT;
|
||||
}
|
||||
|
||||
export function isModalSelect(component: ComponentBase): component is ModalSubmitInteractionDataStringSelectComponent {
|
||||
return component.type === Constants.ComponentTypes.STRING_SELECT;
|
||||
}
|
||||
|
||||
export function componentHasIdPrefix(component: ComponentBase, prefix: string): boolean {
|
||||
return (isModalTextInput(component) || isModalSelect(component)) && component.custom_id.startsWith(prefix);
|
||||
}
|
||||
2
packages/discord/src/components/index.ts
Normal file
2
packages/discord/src/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './helpers';
|
||||
export * from './builders';
|
||||
54
packages/discord/src/core/bot.ts
Normal file
54
packages/discord/src/core/bot.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { importCommands, initializeCommandHandling, registerCommands } from '@commands';
|
||||
import { Client } from '@projectdysnomia/dysnomia';
|
||||
import kv, { asyncKV } from '@star-kitten/util/kv.js';
|
||||
import type { KVStore } from './kv-store.type.ts';
|
||||
import type { Cache } from './cache.type.ts';
|
||||
|
||||
export interface DiscordBotOptions {
|
||||
token?: string;
|
||||
intents?: number[];
|
||||
commandPattern?: string;
|
||||
commandBaseDir?: string;
|
||||
keyStore?: KVStore;
|
||||
cache?: Cache;
|
||||
onError?: (error: Error) => void;
|
||||
onReady?: () => void;
|
||||
}
|
||||
|
||||
export function startDiscordBot({
|
||||
token = process.env.DISCORD_BOT_TOKEN || '',
|
||||
intents = [],
|
||||
commandPattern = '**/*.command.{js,ts}',
|
||||
commandBaseDir = 'src',
|
||||
keyStore = asyncKV,
|
||||
cache = kv,
|
||||
onError,
|
||||
onReady,
|
||||
}: DiscordBotOptions = {}): Client {
|
||||
const client = new Client(`Bot ${token}`, {
|
||||
gateway: {
|
||||
intents,
|
||||
},
|
||||
});
|
||||
|
||||
client.on('ready', async () => {
|
||||
console.debug(`Logged in as ${client.user?.username}#${client.user?.discriminator}`);
|
||||
onReady?.();
|
||||
const commands = await importCommands(commandPattern, commandBaseDir);
|
||||
await registerCommands(
|
||||
client,
|
||||
Object.values(commands).map((cmd) => cmd.definition),
|
||||
);
|
||||
initializeCommandHandling(commands, { client, cache, kv: keyStore });
|
||||
console.debug('Bot is ready and command handling is initialized.');
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
console.error('An error occurred:', error);
|
||||
onError?.(error);
|
||||
});
|
||||
|
||||
client.connect().catch(console.error);
|
||||
|
||||
return client;
|
||||
}
|
||||
6
packages/discord/src/core/cache.type.ts
Normal file
6
packages/discord/src/core/cache.type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Cache {
|
||||
get: <T>(key: string) => T | undefined;
|
||||
set: <T>(key: string, value: T, ttl?: number | string) => boolean;
|
||||
del: (key: string | string[]) => number;
|
||||
has: (key: string) => boolean;
|
||||
}
|
||||
3
packages/discord/src/core/index.ts
Normal file
3
packages/discord/src/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './bot';
|
||||
export * from './cache.type';
|
||||
export * from './kv-store.type.ts';
|
||||
7
packages/discord/src/core/kv-store.type.ts.ts
Normal file
7
packages/discord/src/core/kv-store.type.ts.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface KVStore {
|
||||
get: <T>(key: string) => Promise<T | undefined>;
|
||||
set: (key: string, value: any) => Promise<boolean>;
|
||||
delete: (key: string) => Promise<number>;
|
||||
has: (key: string) => Promise<boolean>;
|
||||
clear: () => Promise<void>;
|
||||
}
|
||||
4
packages/discord/src/index.ts
Normal file
4
packages/discord/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './locales';
|
||||
export * from './commands';
|
||||
export * from './core';
|
||||
export * from './jsx';
|
||||
7
packages/discord/src/jsd/createElement.ts
Normal file
7
packages/discord/src/jsd/createElement.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function createElement(tag: string, attrs: Record<string, any> = {}, ...children: any[]) {
|
||||
return {
|
||||
tag,
|
||||
attrs,
|
||||
children,
|
||||
};
|
||||
}
|
||||
2
packages/discord/src/jsd/index.ts
Normal file
2
packages/discord/src/jsd/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './parser';
|
||||
export * from './createElement';
|
||||
10
packages/discord/src/jsd/parser.test.ts
Normal file
10
packages/discord/src/jsd/parser.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { parseJSDFile } from './parser_new';
|
||||
import path from 'node:path';
|
||||
|
||||
describe('parseJSDFile', () => {
|
||||
it('should parse a JSD file', async () => {
|
||||
const result = await parseJSDFile(path.join(__dirname, '../../fixtures/jsd/test.tsd'));
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
97
packages/discord/src/jsd/parser.ts
Normal file
97
packages/discord/src/jsd/parser.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import parse, { type DOMNode } from 'html-dom-parser';
|
||||
import type { ChildNode } from 'domhandler';
|
||||
|
||||
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
|
||||
|
||||
export async function parseJSDFile(filename: string) {
|
||||
const content = (await fs.readFile(filename)).toString();
|
||||
|
||||
const matches = JSD_STRING.exec(content);
|
||||
if (matches) {
|
||||
let html = matches[1] + '>';
|
||||
const root = parse(html);
|
||||
const translated = translate(root[0]);
|
||||
const str = content.replace(matches[1] + '>', translated);
|
||||
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
interface state {
|
||||
inInterpolation?: boolean;
|
||||
children?: string[][];
|
||||
parent?: Text[];
|
||||
}
|
||||
|
||||
function translate(root: DOMNode | ChildNode | null, state: state = {}): string | null {
|
||||
if (!root || typeof root !== 'object') return null;
|
||||
|
||||
let children = [];
|
||||
if ('children' in root && Array.isArray(root.children) && root.children.length > 0) {
|
||||
for (const child of root.children) {
|
||||
const translated = translate(child, state);
|
||||
if (translated) {
|
||||
if (state.inInterpolation && state.parent[state.children.length - 1] === child) {
|
||||
state.children[state.children.length - 1].push(translated);
|
||||
} else {
|
||||
children.push(translated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ('nodeType' in root && root.nodeType === 3) {
|
||||
if (root.data.trim() === '') return null;
|
||||
return parseText(root.data.trim(), state, root);
|
||||
}
|
||||
|
||||
if ('name' in root && root.name) {
|
||||
let tagName = root.name || 'unknown';
|
||||
let attrs = 'attribs' in root ? root.attribs : {};
|
||||
return `StarKitten.createElement("${tagName}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
|
||||
}
|
||||
}
|
||||
|
||||
const JSD_INTERPOLATION = /\{(.+)\}/gs;
|
||||
const JSD_START_EXP_INTERPOLATION = /\{(.+)\(/gs;
|
||||
const JSD_END_EXP_INTERPOLATION = /\)(.+)\}/gs;
|
||||
|
||||
function parseText(text: string, state: state = {}, parent: Text = {}): string {
|
||||
let interpolations = text.match(JSD_INTERPOLATION);
|
||||
if (!interpolations) {
|
||||
if (text.match(JSD_START_EXP_INTERPOLATION)) {
|
||||
state.inInterpolation = true;
|
||||
state.children = state.children || [[]];
|
||||
state.parent = state.parent || [];
|
||||
state.parent.push(parent);
|
||||
return text.substring(1, text.length - 1);
|
||||
} else if (text.match(JSD_END_EXP_INTERPOLATION)) {
|
||||
const combined = state.children?.[state.children.length - 1].join(' ');
|
||||
state.children?.[state.children.length - 1].splice(0);
|
||||
state.children?.pop();
|
||||
state.parent?.pop();
|
||||
if (state.children.length === 0) {
|
||||
state.inInterpolation = false;
|
||||
return combined + ' ' + text.substring(1, text.length - 1);
|
||||
}
|
||||
}
|
||||
return `"${text}"`;
|
||||
} else {
|
||||
text = replaceInterpolations(text);
|
||||
return `"${text}"`;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceInterpolations(text: string, isOnJSON: boolean = false) {
|
||||
let interpolations = null;
|
||||
|
||||
while ((interpolations = JSD_INTERPOLATION.exec(text))) {
|
||||
if (isOnJSON) {
|
||||
text = text.replace(`"{${interpolations[1]}}"`, interpolations[1]);
|
||||
} else {
|
||||
text = text.replace(`{${interpolations[1]}}`, `"+ ${interpolations[1]} +"`);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
101
packages/discord/src/jsd/parser_new.ts
Normal file
101
packages/discord/src/jsd/parser_new.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import * as acorn from 'acorn';
|
||||
import jsx from 'acorn-jsx';
|
||||
|
||||
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
|
||||
|
||||
const parser = acorn.Parser.extend(jsx());
|
||||
|
||||
export async function parseJSDFile(filename: string) {
|
||||
const content = (await fs.readFile(filename)).toString();
|
||||
|
||||
const matches = JSD_STRING.exec(content);
|
||||
if (matches) {
|
||||
const jsxc = matches[1] + '>';
|
||||
const ast = parser.parse(jsxc, { ecmaVersion: 2020, sourceType: 'module' });
|
||||
const translated = traverseJSX((ast.body[0] as any).expression);
|
||||
const str = content.replace(matches[1] + '>', translated);
|
||||
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function traverseJSX(node: any): string {
|
||||
if (node.type === 'JSXElement') {
|
||||
const tag = node.openingElement.name.name;
|
||||
const attrs: Record<string, any> = {};
|
||||
for (const attr of node.openingElement.attributes) {
|
||||
if (attr.type === 'JSXAttribute') {
|
||||
const name = attr.name.name;
|
||||
const value = attr.value;
|
||||
if (value.type === 'Literal') {
|
||||
attrs[name] = value.value;
|
||||
} else if (value.type === 'JSXExpressionContainer') {
|
||||
attrs[name] = `{${generateCode(value.expression)}}`;
|
||||
} else if (value) {
|
||||
attrs[name] = value.raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
const children = [];
|
||||
for (const child of node.children) {
|
||||
const translated = traverseJSX(child);
|
||||
if (translated) {
|
||||
children.push(translated);
|
||||
}
|
||||
}
|
||||
return `StarKitten.createElement("${tag}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
|
||||
} else if (node.type === 'JSXExpressionContainer') {
|
||||
const expr = generateCode(node.expression);
|
||||
if (node.expression.type === 'TemplateLiteral' || (node.expression.type === 'Literal' && typeof node.expression.value === 'string')) {
|
||||
return `""+ ${expr} +""`;
|
||||
} else {
|
||||
return expr;
|
||||
}
|
||||
} else if (node.type === 'JSXText') {
|
||||
const text = node.value.trim();
|
||||
if (text) {
|
||||
return `"${text}"`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function generateCode(node: any): string {
|
||||
if (node.type === 'JSXElement') {
|
||||
return traverseJSX(node);
|
||||
} else if (node.type === 'Identifier') {
|
||||
return node.name;
|
||||
} else if (node.type === 'Literal') {
|
||||
return JSON.stringify(node.value);
|
||||
} else if (node.type === 'TemplateLiteral') {
|
||||
const quasis = node.quasis.map((q: any) => q.value.raw);
|
||||
const expressions = node.expressions.map((e: any) => generateCode(e));
|
||||
let result = quasis[0];
|
||||
for (let i = 0; i < expressions.length; i++) {
|
||||
result += '${' + expressions[i] + '}' + quasis[i + 1];
|
||||
}
|
||||
return '`' + result + '`';
|
||||
} else if (node.type === 'MemberExpression') {
|
||||
const op = node.optional ? '?.' : '.';
|
||||
return generateCode(node.object) + op + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
|
||||
} else if (node.type === 'OptionalMemberExpression') {
|
||||
return generateCode(node.object) + '?.' + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
|
||||
} else if (node.type === 'CallExpression') {
|
||||
return generateCode(node.callee) + '(' + node.arguments.map((a: any) => generateCode(a)).join(', ') + ')';
|
||||
} else if (node.type === 'BinaryExpression') {
|
||||
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
|
||||
} else if (node.type === 'ConditionalExpression') {
|
||||
return generateCode(node.test) + ' ? ' + generateCode(node.consequent) + ' : ' + generateCode(node.alternate);
|
||||
} else if (node.type === 'LogicalExpression') {
|
||||
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
|
||||
} else if (node.type === 'UnaryExpression') {
|
||||
return node.operator + generateCode(node.argument);
|
||||
} else if (node.type === 'ObjectExpression') {
|
||||
return '{' + node.properties.map((p: any) => generateCode(p.key) + ': ' + generateCode(p.value)).join(', ') + '}';
|
||||
} else if (node.type === 'ArrayExpression') {
|
||||
return '[' + node.elements.map((e: any) => generateCode(e)).join(', ') + ']';
|
||||
} else {
|
||||
return node.raw || node.name || 'unknown';
|
||||
}
|
||||
}
|
||||
5
packages/discord/src/jsx/components/action-row.ts
Normal file
5
packages/discord/src/jsx/components/action-row.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createActionRow } from '@components';
|
||||
|
||||
export function ActionRow(props: { children: any | any[] }) {
|
||||
return createActionRow(...(Array.isArray(props.children) ? props.children : [props.children]));
|
||||
}
|
||||
6
packages/discord/src/jsx/components/button.ts
Normal file
6
packages/discord/src/jsx/components/button.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createButton, type ButtonStyle } from '@components';
|
||||
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
|
||||
|
||||
export function Button(props: { label: string; customId: string; style?: ButtonStyle; emoji?: PartialEmoji; disabled?: boolean }) {
|
||||
return createButton(props.label, props.customId, { style: props.style, emoji: props.emoji, disabled: props.disabled });
|
||||
}
|
||||
8
packages/discord/src/jsx/components/container.ts
Normal file
8
packages/discord/src/jsx/components/container.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createContainer } from '@components';
|
||||
|
||||
export function Container(props: { accent?: number; spoiler?: boolean; children: any | any[] }) {
|
||||
return createContainer(
|
||||
{ accent_color: props.accent, spoiler: props.spoiler },
|
||||
...(Array.isArray(props.children) ? props.children : [props.children]),
|
||||
);
|
||||
}
|
||||
4
packages/discord/src/jsx/components/index.ts
Normal file
4
packages/discord/src/jsx/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './action-row';
|
||||
export * from './button';
|
||||
export * from './container';
|
||||
export * from './text-display';
|
||||
5
packages/discord/src/jsx/components/text-display.ts
Normal file
5
packages/discord/src/jsx/components/text-display.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createTextDisplay } from '@components/builders';
|
||||
|
||||
export function TextDisplay(props: { content: string }) {
|
||||
return createTextDisplay(props.content);
|
||||
}
|
||||
3
packages/discord/src/jsx/index.ts
Normal file
3
packages/discord/src/jsx/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './runtime';
|
||||
export * from './components';
|
||||
export * as JSX from './jsx';
|
||||
69
packages/discord/src/jsx/jsx.ts
Normal file
69
packages/discord/src/jsx/jsx.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
type ActionRow,
|
||||
type Button,
|
||||
type ChannelSelectMenu,
|
||||
type MentionableSelectMenu,
|
||||
type PartialEmoji,
|
||||
type RoleSelectMenu,
|
||||
type StringSelectMenu,
|
||||
type TextInput,
|
||||
type UserSelectMenu,
|
||||
type LabelComponent,
|
||||
type ContainerComponent,
|
||||
type TextDisplayComponent,
|
||||
type SectionComponent,
|
||||
type MediaGalleryComponent,
|
||||
type SeparatorComponent,
|
||||
type FileComponent,
|
||||
type InteractionButton,
|
||||
type URLButton,
|
||||
type PremiumButton,
|
||||
type ThumbnailComponent,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
export type Component =
|
||||
| ActionRow
|
||||
| Button
|
||||
| StringSelectMenu
|
||||
| UserSelectMenu
|
||||
| RoleSelectMenu
|
||||
| MentionableSelectMenu
|
||||
| ChannelSelectMenu
|
||||
| TextInput
|
||||
| LabelComponent
|
||||
| ContainerComponent
|
||||
| TextDisplayComponent
|
||||
| SectionComponent
|
||||
| MediaGalleryComponent
|
||||
| SeparatorComponent
|
||||
| FileComponent
|
||||
| InteractionButton
|
||||
| URLButton
|
||||
| PremiumButton
|
||||
| ThumbnailComponent;
|
||||
|
||||
export type Element = Component | Promise<Component>;
|
||||
|
||||
export interface ElementClass {
|
||||
render: any;
|
||||
}
|
||||
|
||||
export interface ElementAttributesProperty {
|
||||
props: {};
|
||||
}
|
||||
|
||||
export interface IntrinsicElements {
|
||||
// Allow any element, but prefer known elements
|
||||
[elemName: string]: any;
|
||||
// Known elements
|
||||
ActionRow: { children: any | any[] };
|
||||
Button: {
|
||||
label: string;
|
||||
customId: string;
|
||||
style?: number;
|
||||
emoji?: PartialEmoji;
|
||||
disabled?: boolean;
|
||||
};
|
||||
Container: { color?: string; accent?: number; spoiler?: boolean; children: any | any[] };
|
||||
TextDisplay: { content: string };
|
||||
}
|
||||
30
packages/discord/src/jsx/runtime.ts
Normal file
30
packages/discord/src/jsx/runtime.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export function jsx(type: any, props: Record<string, any>) {
|
||||
console.log('JSX', type, props);
|
||||
if (typeof type === 'function') {
|
||||
return type(props);
|
||||
}
|
||||
return {
|
||||
type,
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
||||
export function jsxDEV(
|
||||
type: any,
|
||||
props: Record<string, any>,
|
||||
key: string | number | symbol,
|
||||
isStaticChildren: boolean,
|
||||
source: any,
|
||||
self: any,
|
||||
) {
|
||||
console.log('JSX DEV', type, props);
|
||||
if (typeof type === 'function') {
|
||||
return type(props);
|
||||
}
|
||||
return {
|
||||
type,
|
||||
props: { ...props, key },
|
||||
_source: source,
|
||||
_self: self,
|
||||
};
|
||||
}
|
||||
8
packages/discord/src/jsx/types.d.ts
vendored
Normal file
8
packages/discord/src/jsx/types.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Component, IntrinsicElements as StarKittenIntrinsicElements } from './jsx';
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
type Element = Component;
|
||||
interface IntrinsicElements extends StarKittenIntrinsicElements {}
|
||||
}
|
||||
}
|
||||
26
packages/discord/src/locales.ts
Normal file
26
packages/discord/src/locales.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export type Locales = 'en' | 'ru' | 'de' | 'fr' | 'ja' | 'es' | 'zh' | 'ko';
|
||||
export const ALL_LOCALES: Locales[] = ['en', 'ru', 'de', 'fr', 'ja', 'es', 'zh', 'ko'];
|
||||
export const DEFAULT_LOCALE: Locales = 'en';
|
||||
export const LOCALE_NAMES: { [key in Locales]: string } = {
|
||||
en: 'English',
|
||||
ru: 'Русский',
|
||||
de: 'Deutsch',
|
||||
fr: 'Français',
|
||||
ja: '日本語',
|
||||
es: 'Español',
|
||||
zh: '中文',
|
||||
ko: '한국어',
|
||||
};
|
||||
export function toDiscordLocale(locale: Locales): string {
|
||||
switch (locale) {
|
||||
case 'en': return 'en-US';
|
||||
case 'ru': return 'ru';
|
||||
case 'de': return 'de';
|
||||
case 'fr': return 'fr';
|
||||
case 'ja': return 'ja';
|
||||
case 'es': return 'es-ES';
|
||||
case 'zh': return 'zh-CN';
|
||||
case 'ko': return 'ko';
|
||||
default: return 'en-US';
|
||||
}
|
||||
}
|
||||
2
packages/discord/src/pages/index.ts
Normal file
2
packages/discord/src/pages/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './pages';
|
||||
export * from './subroutes';
|
||||
166
packages/discord/src/pages/pages.ts
Normal file
166
packages/discord/src/pages/pages.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { isAutocomplete, isMessageComponent, isModalSubmit, isPing, type CommandContext } from '@commands';
|
||||
import {
|
||||
Constants,
|
||||
type InteractionContentEdit,
|
||||
type InteractionModalContent,
|
||||
type CommandInteraction,
|
||||
type ComponentInteraction,
|
||||
type ModalSubmitInteraction,
|
||||
Interaction,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
export type PagesInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction;
|
||||
export enum PageType {
|
||||
MODAL = 'modal',
|
||||
MESSAGE = 'message',
|
||||
FOLLOWUP = 'followup',
|
||||
}
|
||||
|
||||
export interface Page<T> {
|
||||
key: string;
|
||||
type?: PageType; // defaults to MESSAGE
|
||||
followUpFlags?: number;
|
||||
render: (
|
||||
ctx: PageContext<T>,
|
||||
) => (InteractionModalContent | InteractionContentEdit) | Promise<InteractionModalContent | InteractionContentEdit>;
|
||||
}
|
||||
|
||||
export interface PagesOptions<T> {
|
||||
pages: Record<string, Page<T>>;
|
||||
initialPage?: string;
|
||||
timeout?: number; // in seconds
|
||||
ephemeral?: boolean; // whether the initial message should be ephemeral
|
||||
useEmbeds?: boolean; // will not enable components v2
|
||||
initialStateData?: T; // initial state to merge with default state
|
||||
router?: (ctx: PageContext<T>) => string; // function to determine the next page key
|
||||
}
|
||||
|
||||
export interface PageState<T> {
|
||||
currentPage: string;
|
||||
timeoutAt: number; // timestamp in ms
|
||||
lastInteractionAt?: number; // timestamp in ms
|
||||
messageId?: string;
|
||||
channelId?: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PageContext<T> {
|
||||
state: PageState<T>;
|
||||
custom_id: string; // current interaction custom_id
|
||||
interaction: PagesInteraction;
|
||||
goToPage: (pageKey: string) => Promise<InteractionContentEdit>;
|
||||
}
|
||||
|
||||
function createPageContext<T>(interaction: PagesInteraction, options: PagesOptions<T>, state: PageState<T>): PageContext<T> {
|
||||
return {
|
||||
state,
|
||||
interaction,
|
||||
custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : (options.initialPage ?? 'root'),
|
||||
goToPage: (pageKey: string) => {
|
||||
const page = options.pages[pageKey];
|
||||
this.state.currentPage = pageKey;
|
||||
if (!page) {
|
||||
throw new Error(`Page with key "${pageKey}" not found`);
|
||||
}
|
||||
return page.render(createPageContext(interaction, options, { ...state, currentPage: pageKey })) as Promise<InteractionContentEdit>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function defaultPageState<T>(options: PagesOptions<T>): PageState<T> {
|
||||
const timeoutAt = options.timeout ? Date.now() + options.timeout * 1000 : Infinity;
|
||||
return {
|
||||
currentPage: options.initialPage ?? options.pages[0].key,
|
||||
timeoutAt,
|
||||
lastInteractionAt: Date.now(),
|
||||
data: options.initialStateData ?? ({} as T),
|
||||
};
|
||||
}
|
||||
|
||||
function getPageState<T>(options: PagesOptions<T>, cmdCtx: CommandContext & { state: { __pageState?: PageState<T> } }) {
|
||||
const cmdState = cmdCtx.state;
|
||||
if ('__pageState' in cmdState && cmdState.__pageState) {
|
||||
return cmdState.__pageState as PageState<T>;
|
||||
}
|
||||
cmdState.__pageState = defaultPageState(options);
|
||||
return cmdState.__pageState as PageState<T>;
|
||||
}
|
||||
|
||||
function validateOptions<T>(options: PagesOptions<T>) {
|
||||
const keys = Object.keys(options.pages);
|
||||
const uniqueKeys = new Set(keys);
|
||||
if (uniqueKeys.size !== keys.length) {
|
||||
throw new Error('Duplicate page keys found');
|
||||
}
|
||||
}
|
||||
|
||||
function getFlags(options: PagesOptions<any>) {
|
||||
let flags = 0;
|
||||
if (options.ephemeral) {
|
||||
flags |= Constants.MessageFlags.EPHEMERAL;
|
||||
}
|
||||
if (!options.useEmbeds) {
|
||||
flags |= Constants.MessageFlags.IS_COMPONENTS_V2;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
export async function usePages<T>(options: PagesOptions<T>, interaction: Interaction, cmdCtx: CommandContext) {
|
||||
if (isAutocomplete(interaction) || isPing(interaction)) {
|
||||
throw new Error('usePages cannot be used with autocomplete or ping interactions');
|
||||
}
|
||||
const pagesInteraction = interaction as PagesInteraction;
|
||||
validateOptions(options);
|
||||
const pageState = getPageState(options, cmdCtx);
|
||||
const pageContext = createPageContext(pagesInteraction, options, pageState);
|
||||
const pageKey =
|
||||
options.router ? options.router(pageContext) : (pageContext.custom_id ?? options.initialPage ?? Object.keys(options.pages)[0]);
|
||||
// if we have subroutes, we only want the main route from the page key
|
||||
const page = options.pages[pageKey.split(':')[0]] ?? options.pages[0];
|
||||
pageContext.state.currentPage = page.key;
|
||||
|
||||
if (page.type === PageType.MODAL && !isModalSubmit(pagesInteraction)) {
|
||||
// we don't defer modals and can't respond to a modal with a modal.
|
||||
const cnt = page.render(pageContext);
|
||||
const content = isPromise(cnt) ? await cnt : cnt;
|
||||
return await pagesInteraction.createModal(content as InteractionModalContent);
|
||||
}
|
||||
|
||||
if (page.type === PageType.FOLLOWUP) {
|
||||
if (!pageState.messageId) {
|
||||
throw new Error('Cannot send a followup message before an initial message has been sent');
|
||||
}
|
||||
const flags = page.type === PageType.FOLLOWUP ? (page.followUpFlags ?? getFlags(options)) : getFlags(options);
|
||||
await pagesInteraction.defer(flags);
|
||||
const cnt = page.render(pageContext);
|
||||
const content = isPromise(cnt) ? await cnt : cnt;
|
||||
return await pagesInteraction.createFollowup({
|
||||
flags,
|
||||
...(content as InteractionContentEdit),
|
||||
});
|
||||
}
|
||||
|
||||
if (pageState.messageId && isMessageComponent(pagesInteraction)) {
|
||||
await pagesInteraction.deferUpdate();
|
||||
const cnt = page.render(pageContext);
|
||||
const content = isPromise(cnt) ? await cnt : cnt;
|
||||
return await pagesInteraction.editMessage(pageState.messageId, content as InteractionContentEdit);
|
||||
}
|
||||
|
||||
{
|
||||
await pagesInteraction.defer(getFlags(options));
|
||||
const cnt = page.render(pageContext);
|
||||
const content = isPromise(cnt) ? await cnt : cnt;
|
||||
const message = await pagesInteraction.createFollowup({
|
||||
flags: getFlags(options),
|
||||
...(content as InteractionContentEdit),
|
||||
});
|
||||
pageState.messageId = message.id;
|
||||
pageState.channelId = message.channel?.id;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
|
||||
return typeof (value as Promise<T>)?.then === 'function';
|
||||
}
|
||||
99
packages/discord/src/pages/subroutes.ts
Normal file
99
packages/discord/src/pages/subroutes.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
|
||||
import { createActionRow, createButton, createMediaGallery, type ButtonOptions, type ContainerItems } from '@components';
|
||||
import type { PageContext } from './pages';
|
||||
|
||||
export function getSubrouteKey(prefix: string, subroutes: string[]) {
|
||||
return `${prefix}:${subroutes.join(':')}`;
|
||||
}
|
||||
|
||||
export function parseSubrouteKey(key: string, expectedPrefix: string, expectedLength: number, defaults: string[] = []) {
|
||||
const parts = key.split(':');
|
||||
if (parts[0] !== expectedPrefix) {
|
||||
throw new Error(`Unexpected prefix: ${parts[0]}`);
|
||||
}
|
||||
if (parts.length - 1 < expectedLength && defaults.length) {
|
||||
// fill in defaults
|
||||
parts.push(...defaults.slice(parts.length - 1));
|
||||
}
|
||||
if (parts.length !== expectedLength + 1) {
|
||||
throw new Error(`Expected ${expectedLength} subroutes, but got ${parts.length - 1}`);
|
||||
}
|
||||
return parts.slice(1);
|
||||
}
|
||||
|
||||
export function renderSubrouteButtons(
|
||||
currentSubroute: string,
|
||||
subRoutes: string[],
|
||||
subrouteIndex: number,
|
||||
prefix: string,
|
||||
subroutes: { label: string; value: string; emoji?: PartialEmoji }[],
|
||||
options?: Partial<ButtonOptions>,
|
||||
) {
|
||||
return subroutes
|
||||
.filter((sr) => sr !== undefined)
|
||||
.map(({ label, value, emoji }) => {
|
||||
const routes = [...subRoutes];
|
||||
routes[subrouteIndex] = currentSubroute == value ? '_' : value;
|
||||
return createButton(label, getSubrouteKey(prefix, routes), {
|
||||
...options,
|
||||
disabled: value === currentSubroute,
|
||||
emoji,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface SubrouteOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
emoji?: PartialEmoji;
|
||||
}
|
||||
|
||||
export function renderSubroutes<T, CType = ContainerItems>(
|
||||
context: PageContext<T>,
|
||||
prefix: string,
|
||||
subroutes: (SubrouteOptions & {
|
||||
banner?: string;
|
||||
actionRowPosition?: 'top' | 'bottom';
|
||||
})[][],
|
||||
render: (currentSubroute: string, ctx: PageContext<T>) => CType,
|
||||
btnOptions?: Partial<ButtonOptions>,
|
||||
defaultSubroutes?: string[], // if not provided, will use the first option of each subroute
|
||||
): CType[] {
|
||||
const currentSubroutes = parseSubrouteKey(
|
||||
context.custom_id,
|
||||
prefix,
|
||||
subroutes.length,
|
||||
defaultSubroutes || subroutes.map((s) => s[0].value),
|
||||
);
|
||||
|
||||
const components = subroutes
|
||||
.filter((sr) => sr.length > 0)
|
||||
.map((srOpts, index) => {
|
||||
const opts = srOpts.filter((sr) => sr !== undefined);
|
||||
if (opts.length === 0) return undefined;
|
||||
// find the current subroute, or default to the first
|
||||
const sri = opts.findIndex((s) => s.value === currentSubroutes[index]);
|
||||
const current = opts[sri] || opts[0];
|
||||
const components = [];
|
||||
|
||||
const actionRow = createActionRow(...renderSubrouteButtons(current.value, currentSubroutes, index, prefix, opts, btnOptions));
|
||||
|
||||
if (current.banner) {
|
||||
components.push(createMediaGallery({ url: current.banner }));
|
||||
}
|
||||
|
||||
if (!current.actionRowPosition || current.actionRowPosition === 'top') {
|
||||
components.push(actionRow);
|
||||
}
|
||||
|
||||
components.push(render(current.value, context));
|
||||
|
||||
if (current.actionRowPosition === 'bottom') {
|
||||
components.push(actionRow);
|
||||
}
|
||||
return components;
|
||||
})
|
||||
.flat()
|
||||
.filter((c) => c !== undefined);
|
||||
return components;
|
||||
}
|
||||
41
packages/discord/tsconfig.json
Normal file
41
packages/discord/tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@star-kitten/discord",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
// Paths
|
||||
"paths": {
|
||||
"@*": ["./src/*"],
|
||||
"@types": ["./types/*"]
|
||||
},
|
||||
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
||||
"typeRoots": ["src/types", "./node_modules/@types"]
|
||||
},
|
||||
"include": ["src", "types", "src/jsx/types.d.ts"],
|
||||
"exclude": ["node_modules", "dist", "build", "**/*.test.ts"]
|
||||
}
|
||||
17
packages/discord/tsdown.config.ts
Normal file
17
packages/discord/tsdown.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: [
|
||||
'./src/index.ts',
|
||||
'./src/commands/index.ts',
|
||||
'./src/components/index.ts',
|
||||
'./src/pages/index.ts',
|
||||
'./src/common/index.ts',
|
||||
'./src/jsx/index.ts',
|
||||
],
|
||||
platform: 'node',
|
||||
dts: true,
|
||||
external: ['bun', 'bun:sqlite'],
|
||||
},
|
||||
]);
|
||||
65
packages/discord/types/index.d.ts
vendored
Normal file
65
packages/discord/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
type ActionRow,
|
||||
type Button,
|
||||
type ChannelSelectMenu,
|
||||
type GuildChannelTypes,
|
||||
type MentionableSelectMenu,
|
||||
type PartialEmoji,
|
||||
type RoleSelectMenu,
|
||||
type StringSelectMenu,
|
||||
type TextInput,
|
||||
type UserSelectMenu,
|
||||
type LabelComponent,
|
||||
type ContainerComponent,
|
||||
type TextDisplayComponent,
|
||||
type SectionComponent,
|
||||
type MediaGalleryComponent,
|
||||
type SeparatorComponent,
|
||||
type FileComponent,
|
||||
type InteractionButton,
|
||||
type URLButton,
|
||||
type PremiumButton,
|
||||
type ThumbnailComponent,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
declare namespace JSX {
|
||||
type Component =
|
||||
| Button
|
||||
| StringSelectMenu
|
||||
| UserSelectMenu
|
||||
| RoleSelectMenu
|
||||
| MentionableSelectMenu
|
||||
| ChannelSelectMenu
|
||||
| TextInput
|
||||
| LabelComponent
|
||||
| ContainerComponent
|
||||
| TextDisplayComponent
|
||||
| SectionComponent
|
||||
| MediaGalleryComponent
|
||||
| SeparatorComponent
|
||||
| FileComponent
|
||||
| InteractionButton
|
||||
| URLButton
|
||||
| PremiumButton
|
||||
| ThumbnailComponent;
|
||||
|
||||
type Element = Component | Promise<Component>;
|
||||
|
||||
interface ElementClass {
|
||||
render: any;
|
||||
}
|
||||
|
||||
interface ElementAttributesProperty {
|
||||
props: {};
|
||||
}
|
||||
|
||||
interface IntrinsicElements {
|
||||
// Allow any element, but prefer known elements
|
||||
[elemName: string]: any;
|
||||
// Known elements
|
||||
ActionRow: { children: any | any[] };
|
||||
Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean };
|
||||
Container: { accent?: number; spoiler?: boolean; children: any | any[] };
|
||||
TextDisplay: { content: string };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user