Compare commits

..

37 Commits

Author SHA1 Message Date
github-actions[bot]
f6afafa352 chore(main): release 2.5.0 (#205)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-01-30 12:30:28 -06:00
Jacob Nguyen
b4b195dc95 chore: release 2.5.0
Release-As: 2.5.0
2023-01-30 12:27:18 -06:00
Jacob Nguyen
33f14467ec feat!: simpler plugins (#193)
* feat: experimental plugin changes

* more refactors and name changes

* feat: update name usage and update dispatchers.ts

* fix:naming

* feat: slightly safer typings than any[]

* fix: forgot to destructure arguments

* feat: add special function

* fix: typings

* feat: SUPER SIMPLIFY!!!

* refactor: move promisifiedPlugins closer to call site

* refactor: typings

* refactor: typings

* refactor: consolidate resolving initplugins into one function

* refactor: better types

* revert: remove unneeded function

* revert: remove unneeded function

* feat: dispatch work, simplify

* feat: move some observableHandling function to operators for clarity

* feat: simplify and document

* feat: simplifying sern and docs

* fix: typings

* docs: clarity of function name

* docs: add documentation for executeModule

* feat: contextArgs overloads

* docs: found out why

* fix: typings

* feat: shorten operators signature

* refactor: switch to correct convention

* refactor: take(1) -> first()

* refactor: revert

* refactor: safer typings (less any) and more accurate typings

* style: prettier and short type aliases

* fix: typings

* fix: typings

* docs: add deprecations

* refactor: organization and moving stuff

* pretty: prettey

* docs: describe file

* chore: update dependencies and version

* docs: fix link for docasaurus

* refactor: using a more appropriate operator function for closing an observable on crash

* fix!: changing single and many

* refactor: typings and simplifying composeRoot

* fix: re-add logger into handleError

* docs: comment

* docs: new section

* feat: help mitigate breaking changes

* feat: help mitigate breaking changes

* feat: help mitigate breaking changes and function overloads

* feat: deprecate instead of remove

* feat: partial remove and deprecate old symbols

* revert: trying to accommodate old plugins is too difficult

* docs: add many as deprecated

* docs: update

* feat: partial backwards compatability

* refactor: renaming, docs, and exports more clean

* refactor: context got a lot simpler

* refactor: imports

* docs: explain methods
2023-01-26 12:06:04 -06:00
renovate[bot]
cb95105c1c chore(deps): update dependency prettier to v2.8.3 (#196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-17 12:54:57 -06:00
renovate[bot]
845b82feef chore(deps): update actions/checkout digest to ac59398 (#184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-14 22:38:44 -06:00
renovate[bot]
833a323f3c chore(deps): update actions/setup-node digest to 64ed1c7 (#185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-14 22:36:47 -06:00
renovate[bot]
42e5f20425 chore(deps): update pnpm to v7.25.0 (#195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-14 22:36:09 -06:00
renovate[bot]
2b25e6bfbb chore(deps): update pnpm to v7.22.0 (#179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-07 19:43:59 -06:00
renovate[bot]
1a27341092 chore(deps): update dependency prettier to v2.8.2 (#189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-01-07 16:30:48 -06:00
renovate[bot]
4680e451bb chore(deps): update dependency @typescript-eslint/parser to v5.48.0 (#188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-07 16:09:49 -06:00
renovate[bot]
917d8b0d1b chore(deps): update dependency iti to ^0.6.0 (#178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-31 14:01:56 -06:00
github-actions[bot]
b8492ee45d chore(main): release 2.1.1 (#181)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-12-31 13:50:44 -06:00
Jacob Nguyen
08aac1d67a chore: bump version 2022-12-31 13:47:50 -06:00
Jacob Nguyen
a13df6fb42 fix: modals remapping 2022-12-31 13:41:20 -06:00
Jacob Nguyen
7089f5c0dc chore: fix prettier wkflw holy shit 2022-12-30 11:30:38 -06:00
Jacob Nguyen
559c1a7a7b chore: fix prettier wkflw holy shit 2022-12-30 11:28:50 -06:00
Jacob Nguyen
ac27d168e2 chore: fix prettier wkflw holy shit 2022-12-30 11:27:00 -06:00
Jacob Nguyen
d1e6ec4589 chore: fix prettier wkflw holy shit 2022-12-30 11:25:42 -06:00
Jacob Nguyen
ff379d03be chore: fix prettier wkflw holy shit 2022-12-30 11:15:40 -06:00
Jacob Nguyen
1e4e933db2 chore: fix prettier wkflw holy shit 2022-12-30 11:13:38 -06:00
Jacob Nguyen
ce960f4c8d chore: fix prettier wkflw holy shit 2022-12-30 11:11:38 -06:00
Jacob Nguyen
1130456045 chore: fix prettier wkflw holy shit 2022-12-30 11:10:48 -06:00
Jacob Nguyen
1617d2dcc3 chore: fix prettier wkflw 2022-12-30 11:09:14 -06:00
Jacob Nguyen
ddacbd6e38 chore: fix prettier wkflw 2022-12-30 11:06:26 -06:00
Jacob Nguyen
d69819e9fc chore: fix prettier wkflw 2022-12-30 11:03:30 -06:00
github-actions[bot]
49e4ba623f chore(main): release 2.1.0 (#176)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-30 10:58:57 -06:00
github-actions[bot]
1b6c413fc2 style: pretty please (#175)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2022-12-30 10:56:23 -06:00
Jacob Nguyen
e986535935 fix: multi parameter events 2022-12-30 10:45:02 -06:00
Jacob Nguyen
c30aac476c feat: grammar 2022-12-28 15:37:35 -06:00
github-actions[bot]
f9622d3788 style: pretty please (#173)
Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
2022-12-28 15:34:22 -06:00
renovate[bot]
f286a24686 chore(deps): update dependency prettier to v2.8.1 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:20:45 -06:00
renovate[bot]
166934d749 chore(deps): update dependency @typescript-eslint/eslint-plugin to v5.47.1 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:18:51 -06:00
renovate[bot]
01d79177e8 chore(deps): lock file maintenance (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:15:12 -06:00
renovate[bot]
50dac7fb46 chore(deps): update dependency @typescript-eslint/parser to v5.47.1 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:10:17 -06:00
renovate[bot]
714d23d401 chore(deps): update actions/checkout digest to 755da8c (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 15:05:10 -06:00
renovate[bot]
565c4fc35a chore(deps): update dependency eslint to v8.30.0 (#152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-28 14:56:44 -06:00
github-actions[bot]
8d18c4b182 style: pretty please (#162)
* style: pretty please

* feat: no package.lock.json anymore

Co-authored-by: jacoobes <jacoobes@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2022-12-28 14:40:19 -06:00
51 changed files with 1423 additions and 1263 deletions

View File

@@ -7,7 +7,7 @@
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"semi": ["error", "always"],
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/ban-types" : 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-explicit-any": "off"
}
}

View File

@@ -17,19 +17,22 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
- name: Install pnpm
run: npm i -g pnpm
# Prettier must be in `package.json`
- name: Install Node.js dependencies
run: npm i -g prettier && npm i
run: pnpm i
- name: Run Prettier
run: prettier --write .
run: pnpm run pretty
- name: Create Pull Request
id: cpr

View File

@@ -1,5 +1,40 @@
# Changelog
## [2.5.0](https://github.com/sern-handler/handler/compare/v2.1.1...v2.5.0) (2023-01-30)
### ⚠ BREAKING CHANGES
* simpler plugins ([#193](https://github.com/sern-handler/handler/issues/193))
### Features
* simpler plugins ([#193](https://github.com/sern-handler/handler/issues/193)) ([33f1446](https://github.com/sern-handler/handler/commit/33f14467ec413e003a82503c8a77cb42a6194281))
### Miscellaneous Chores
* release 2.5.0 ([b4b195d](https://github.com/sern-handler/handler/commit/b4b195dc9586736760d0b78caa8589f3d6131f8a))
## [2.1.1](https://github.com/sern-handler/handler/compare/v2.1.0...v2.1.1) (2022-12-31)
### Bug Fixes
* modals remapping ([a13df6f](https://github.com/sern-handler/handler/commit/a13df6fb424d256476284da49024dbe56e82baab))
## [2.1.0](https://github.com/sern-handler/handler/compare/v2.0.0...v2.1.0) (2022-12-30)
### Features
* grammar ([c30aac4](https://github.com/sern-handler/handler/commit/c30aac476cdc2094de34f9f67b5805204cc5e4dd))
### Bug Fixes
* multi parameter events ([e986535](https://github.com/sern-handler/handler/commit/e98653593566ef4635493e0c997bd107a7a3a2a2))
## [2.0.0](https://github.com/sern-handler/handler/compare/v1.2.1...v2.0.0) (2022-12-28)

View File

@@ -28,7 +28,13 @@ yarn add @sern/handler
```sh
pnpm add @sern/handler
```
## Why?
- Most handlers don't support discord.js 14.7+
- Customizable commands
- Plug and play or customize to your liking
- Embraces reactive programming for consistent and reliable backend
- Customizable logger, error handling, and more
- Active development and growing [community](https://sern.dev/discord)
## 👀 Quick Look
* Support for discord.js v14 and all interactions
@@ -105,7 +111,7 @@ It is **highly encouraged** to use the [command line interface](https://github.c
## 👋 Contribute
- Read our contribution [guidelines](./.github/CONTRIBUTING.md) carefully
- Read our contribution [guidelines](https://github.com/sern-handler/handler/blob/main/.github/CONTRIBUTING.md) carefully
- Pull up on [issues](https://github.com/sern-handler/handler/issues) and report bugs
- All kinds of contributions are welcomed.

View File

@@ -1,7 +1,7 @@
{
"name": "@sern/handler",
"packageManager": "pnpm@7.18.2",
"version": "2.0.0",
"packageManager": "pnpm@7.25.0",
"version": "2.5.0",
"description": "A customizable, batteries-included, powerful discord.js framework to automate and streamline bot development.",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
@@ -33,22 +33,20 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"iti": "^0.5.0",
"iti": "^0.6.0",
"rxjs": "^7.5.6",
"ts-pattern": "^4.0.2",
"ts-pattern": "^4.0.6",
"ts-results-es": "^3.5.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
"eslint": "8.25.0",
"prettier": "2.7.1",
"@typescript-eslint/eslint-plugin": "5.47.1",
"@typescript-eslint/parser": "5.48.0",
"discord.js": ">= ^14.7.x",
"eslint": "8.30.0",
"prettier": "2.8.3",
"tsup": "^6.1.3",
"typescript": "4.9.4"
},
"peerDependencies": {
"discord.js": ">= ^14.7.x"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/handler.git"

295
pnpm-lock.yaml generated
View File

@@ -1,30 +1,30 @@
lockfileVersion: 5.4
specifiers:
'@typescript-eslint/eslint-plugin': 5.44.0
'@typescript-eslint/parser': 5.44.0
'@typescript-eslint/eslint-plugin': 5.47.1
'@typescript-eslint/parser': 5.48.0
discord.js: '>= ^14.7.x'
eslint: 8.25.0
iti: ^0.5.0
prettier: 2.7.1
eslint: 8.30.0
iti: ^0.6.0
prettier: 2.8.3
rxjs: ^7.5.6
ts-pattern: ^4.0.2
ts-pattern: ^4.0.6
ts-results-es: ^3.5.0
tsup: ^6.1.3
typescript: 4.9.4
dependencies:
discord.js: 14.7.1
iti: 0.5.0
rxjs: 7.7.0
iti: 0.6.0
rxjs: 7.8.0
ts-pattern: 4.0.6
ts-results-es: 3.5.0
devDependencies:
'@typescript-eslint/eslint-plugin': 5.44.0_u4y2jgydxczzu3dhxaxb67oroa
'@typescript-eslint/parser': 5.44.0_grqhnqpocakf5dew66i47isfme
eslint: 8.25.0
prettier: 2.7.1
'@typescript-eslint/eslint-plugin': 5.47.1_ms6clkwmnrnf7qx5hae2o4lcfe
'@typescript-eslint/parser': 5.48.0_lzzuuodtsqwxnvqeq4g4likcqa
discord.js: 14.7.1
eslint: 8.30.0
prettier: 2.8.3
tsup: 6.5.0_typescript@4.9.4
typescript: 4.9.4
@@ -36,35 +36,35 @@ packages:
dependencies:
'@discordjs/util': 0.1.0
'@sapphire/shapeshift': 3.8.1
discord-api-types: 0.37.23
discord-api-types: 0.37.24
fast-deep-equal: 3.1.3
ts-mixer: 6.0.2
tslib: 2.4.1
dev: false
dev: true
/@discordjs/collection/1.3.0:
resolution: {integrity: sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==}
engines: {node: '>=16.9.0'}
dev: false
dev: true
/@discordjs/rest/1.4.0:
resolution: {integrity: sha512-k3Ip7ffFSAfp7Mu4H/3BEXFvFz+JsbXRrRtpeBMnSp1LefhtlZWJE6xdXzNlblktKNQltnRwY+z0NZrGQdxAMw==}
/@discordjs/rest/1.5.0:
resolution: {integrity: sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA==}
engines: {node: '>=16.9.0'}
dependencies:
'@discordjs/collection': 1.3.0
'@discordjs/util': 0.1.0
'@sapphire/async-queue': 1.5.0
'@sapphire/snowflake': 3.3.0
discord-api-types: 0.37.23
'@sapphire/snowflake': 3.4.0
discord-api-types: 0.37.24
file-type: 18.0.0
tslib: 2.4.1
undici: 5.14.0
dev: false
dev: true
/@discordjs/util/0.1.0:
resolution: {integrity: sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==}
engines: {node: '>=16.9.0'}
dev: false
dev: true
/@esbuild/android-arm/0.15.18:
resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
@@ -84,15 +84,15 @@ packages:
dev: true
optional: true
/@eslint/eslintrc/1.3.3:
resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==}
/@eslint/eslintrc/1.4.0:
resolution: {integrity: sha512-7yfvXy6MWLgWSFsLhz5yH3iQ52St8cdUY6FoGieKkRDVxuxmrNuUetIuu6cmjNWwniUHiWXjxCr5tTXDrbYS5A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
ajv: 6.12.6
debug: 4.3.4
espree: 9.4.1
globals: 13.19.0
ignore: 5.2.1
ignore: 5.2.4
import-fresh: 3.3.0
js-yaml: 4.1.0
minimatch: 3.1.2
@@ -101,8 +101,8 @@ packages:
- supports-color
dev: true
/@humanwhocodes/config-array/0.10.7:
resolution: {integrity: sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==}
/@humanwhocodes/config-array/0.11.8:
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
engines: {node: '>=10.10.0'}
dependencies:
'@humanwhocodes/object-schema': 1.2.1
@@ -145,7 +145,7 @@ packages:
/@sapphire/async-queue/1.5.0:
resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
dev: false
dev: true
/@sapphire/shapeshift/3.8.1:
resolution: {integrity: sha512-xG1oXXBhCjPKbxrRTlox9ddaZTvVpOhYLmKmApD/vIWOV1xEYXnpoFs68zHIZBGbqztq6FrUPNPerIrO1Hqeaw==}
@@ -153,24 +153,24 @@ packages:
dependencies:
fast-deep-equal: 3.1.3
lodash: 4.17.21
dev: false
dev: true
/@sapphire/snowflake/3.3.0:
resolution: {integrity: sha512-Hec5N6zEkZuZFLybVKyLFLlcSgYmR6C1/+9NkIhxPwOf6tgX52ndJCSz8ADejmbrNE0VuNCNkpzhRZzenEC9vA==}
/@sapphire/snowflake/3.4.0:
resolution: {integrity: sha512-zZxymtVO6zeXVMPds+6d7gv/OfnCc25M1Z+7ZLB0oPmeMTPeRWVPQSS16oDJy5ZsyCOLj7M6mbZml5gWXcVRNw==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
dev: false
dev: true
/@tokenizer/token/0.3.0:
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
dev: false
dev: true
/@types/json-schema/7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
/@types/node/18.11.15:
resolution: {integrity: sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw==}
dev: false
/@types/node/18.11.18:
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
dev: true
/@types/semver/7.3.13:
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
@@ -179,11 +179,11 @@ packages:
/@types/ws/8.5.3:
resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==}
dependencies:
'@types/node': 18.11.15
dev: false
'@types/node': 18.11.18
dev: true
/@typescript-eslint/eslint-plugin/5.44.0_u4y2jgydxczzu3dhxaxb67oroa:
resolution: {integrity: sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==}
/@typescript-eslint/eslint-plugin/5.47.1_ms6clkwmnrnf7qx5hae2o4lcfe:
resolution: {integrity: sha512-r4RZ2Jl9kcQN7K/dcOT+J7NAimbiis4sSM9spvWimsBvDegMhKLA5vri2jG19PmIPbDjPeWzfUPQ2hjEzA4Nmg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
'@typescript-eslint/parser': ^5.0.0
@@ -193,13 +193,13 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/parser': 5.44.0_grqhnqpocakf5dew66i47isfme
'@typescript-eslint/scope-manager': 5.44.0
'@typescript-eslint/type-utils': 5.44.0_grqhnqpocakf5dew66i47isfme
'@typescript-eslint/utils': 5.44.0_grqhnqpocakf5dew66i47isfme
'@typescript-eslint/parser': 5.48.0_lzzuuodtsqwxnvqeq4g4likcqa
'@typescript-eslint/scope-manager': 5.47.1
'@typescript-eslint/type-utils': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa
'@typescript-eslint/utils': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa
debug: 4.3.4
eslint: 8.25.0
ignore: 5.2.1
eslint: 8.30.0
ignore: 5.2.4
natural-compare-lite: 1.4.0
regexpp: 3.2.0
semver: 7.3.8
@@ -209,8 +209,8 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser/5.44.0_grqhnqpocakf5dew66i47isfme:
resolution: {integrity: sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==}
/@typescript-eslint/parser/5.48.0_lzzuuodtsqwxnvqeq4g4likcqa:
resolution: {integrity: sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -219,26 +219,34 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 5.44.0
'@typescript-eslint/types': 5.44.0
'@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.4
'@typescript-eslint/scope-manager': 5.48.0
'@typescript-eslint/types': 5.48.0
'@typescript-eslint/typescript-estree': 5.48.0_typescript@4.9.4
debug: 4.3.4
eslint: 8.25.0
eslint: 8.30.0
typescript: 4.9.4
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/scope-manager/5.44.0:
resolution: {integrity: sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g==}
/@typescript-eslint/scope-manager/5.47.1:
resolution: {integrity: sha512-9hsFDsgUwrdOoW1D97Ewog7DYSHaq4WKuNs0LHF9RiCmqB0Z+XRR4Pf7u7u9z/8CciHuJ6yxNws1XznI3ddjEw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.44.0
'@typescript-eslint/visitor-keys': 5.44.0
'@typescript-eslint/types': 5.47.1
'@typescript-eslint/visitor-keys': 5.47.1
dev: true
/@typescript-eslint/type-utils/5.44.0_grqhnqpocakf5dew66i47isfme:
resolution: {integrity: sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w==}
/@typescript-eslint/scope-manager/5.48.0:
resolution: {integrity: sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.48.0
'@typescript-eslint/visitor-keys': 5.48.0
dev: true
/@typescript-eslint/type-utils/5.47.1_lzzuuodtsqwxnvqeq4g4likcqa:
resolution: {integrity: sha512-/UKOeo8ee80A7/GJA427oIrBi/Gd4osk/3auBUg4Rn9EahFpevVV1mUK8hjyQD5lHPqX397x6CwOk5WGh1E/1w==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: '*'
@@ -247,23 +255,28 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.4
'@typescript-eslint/utils': 5.44.0_grqhnqpocakf5dew66i47isfme
'@typescript-eslint/typescript-estree': 5.47.1_typescript@4.9.4
'@typescript-eslint/utils': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa
debug: 4.3.4
eslint: 8.25.0
eslint: 8.30.0
tsutils: 3.21.0_typescript@4.9.4
typescript: 4.9.4
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/types/5.44.0:
resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==}
/@typescript-eslint/types/5.47.1:
resolution: {integrity: sha512-CmALY9YWXEpwuu6377ybJBZdtSAnzXLSQcxLSqSQSbC7VfpMu/HLVdrnVJj7ycI138EHqocW02LPJErE35cE9A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@typescript-eslint/typescript-estree/5.44.0_typescript@4.9.4:
resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==}
/@typescript-eslint/types/5.48.0:
resolution: {integrity: sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@typescript-eslint/typescript-estree/5.47.1_typescript@4.9.4:
resolution: {integrity: sha512-4+ZhFSuISAvRi2xUszEj0xXbNTHceV9GbH9S8oAD2a/F9SW57aJNQVOCxG8GPfSWH/X4eOPdMEU2jYVuWKEpWA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
typescript: '*'
@@ -271,8 +284,8 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 5.44.0
'@typescript-eslint/visitor-keys': 5.44.0
'@typescript-eslint/types': 5.47.1
'@typescript-eslint/visitor-keys': 5.47.1
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
@@ -283,31 +296,60 @@ packages:
- supports-color
dev: true
/@typescript-eslint/utils/5.44.0_grqhnqpocakf5dew66i47isfme:
resolution: {integrity: sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw==}
/@typescript-eslint/typescript-estree/5.48.0_typescript@4.9.4:
resolution: {integrity: sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 5.48.0
'@typescript-eslint/visitor-keys': 5.48.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.3.8
tsutils: 3.21.0_typescript@4.9.4
typescript: 4.9.4
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/utils/5.47.1_lzzuuodtsqwxnvqeq4g4likcqa:
resolution: {integrity: sha512-l90SdwqfmkuIVaREZ2ykEfCezepCLxzWMo5gVfcJsJCaT4jHT+QjgSkYhs5BMQmWqE9k3AtIfk4g211z/sTMVw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@types/json-schema': 7.0.11
'@types/semver': 7.3.13
'@typescript-eslint/scope-manager': 5.44.0
'@typescript-eslint/types': 5.44.0
'@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.4
eslint: 8.25.0
'@typescript-eslint/scope-manager': 5.47.1
'@typescript-eslint/types': 5.47.1
'@typescript-eslint/typescript-estree': 5.47.1_typescript@4.9.4
eslint: 8.30.0
eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@8.25.0
eslint-utils: 3.0.0_eslint@8.30.0
semver: 7.3.8
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/@typescript-eslint/visitor-keys/5.44.0:
resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==}
/@typescript-eslint/visitor-keys/5.47.1:
resolution: {integrity: sha512-rF3pmut2JCCjh6BLRhNKdYjULMb1brvoaiWDlHfLNVgmnZ0sBVJrs3SyaKE1XoDDnJuAx/hDQryHYmPUuNq0ig==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.44.0
'@typescript-eslint/types': 5.47.1
eslint-visitor-keys: 3.3.0
dev: true
/@typescript-eslint/visitor-keys/5.48.0:
resolution: {integrity: sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.48.0
eslint-visitor-keys: 3.3.0
dev: true
@@ -405,7 +447,7 @@ packages:
engines: {node: '>=10.16.0'}
dependencies:
streamsearch: 1.1.0
dev: false
dev: true
/cac/6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
@@ -492,9 +534,9 @@ packages:
path-type: 4.0.0
dev: true
/discord-api-types/0.37.23:
resolution: {integrity: sha512-IixodMf9PjOSrsPorbti5aGv6sJVSPJybAlFMYsYDQXFS7zJyhSmA/ofpPTR9ZYqL1KEDY+xU74oZgBQa52eSQ==}
dev: false
/discord-api-types/0.37.24:
resolution: {integrity: sha512-1+Fb4huJCihdbkJLcq2p7nBmtlmAryNwjefT8wwJnL8c7bc7WA87Oaa5mbLe96QvZyfwnwRCDX40H0HhcVV50g==}
dev: true
/discord.js/14.7.1:
resolution: {integrity: sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA==}
@@ -502,11 +544,11 @@ packages:
dependencies:
'@discordjs/builders': 1.4.0
'@discordjs/collection': 1.3.0
'@discordjs/rest': 1.4.0
'@discordjs/rest': 1.5.0
'@discordjs/util': 0.1.0
'@sapphire/snowflake': 3.3.0
'@sapphire/snowflake': 3.4.0
'@types/ws': 8.5.3
discord-api-types: 0.37.23
discord-api-types: 0.37.24
fast-deep-equal: 3.1.3
lodash.snakecase: 4.1.1
tslib: 2.4.1
@@ -515,7 +557,7 @@ packages:
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
dev: true
/doctrine/3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
@@ -755,13 +797,13 @@ packages:
estraverse: 5.3.0
dev: true
/eslint-utils/3.0.0_eslint@8.25.0:
/eslint-utils/3.0.0_eslint@8.30.0:
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
peerDependencies:
eslint: '>=5'
dependencies:
eslint: 8.25.0
eslint: 8.30.0
eslint-visitor-keys: 2.1.0
dev: true
@@ -775,14 +817,15 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/eslint/8.25.0:
resolution: {integrity: sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==}
/eslint/8.30.0:
resolution: {integrity: sha512-MGADB39QqYuzEGov+F/qb18r4i7DohCDOfatHaxI2iGlPuC65bwG2gxgO+7DkyL38dRFaRH7RaRAgU6JKL9rMQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
hasBin: true
dependencies:
'@eslint/eslintrc': 1.3.3
'@humanwhocodes/config-array': 0.10.7
'@eslint/eslintrc': 1.4.0
'@humanwhocodes/config-array': 0.11.8
'@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
@@ -790,7 +833,7 @@ packages:
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.1.1
eslint-utils: 3.0.0_eslint@8.25.0
eslint-utils: 3.0.0_eslint@8.30.0
eslint-visitor-keys: 3.3.0
espree: 9.4.1
esquery: 1.4.0
@@ -800,12 +843,12 @@ packages:
find-up: 5.0.0
glob-parent: 6.0.2
globals: 13.19.0
globby: 11.1.0
grapheme-splitter: 1.0.4
ignore: 5.2.1
ignore: 5.2.4
import-fresh: 3.3.0
imurmurhash: 0.1.4
is-glob: 4.0.3
is-path-inside: 3.0.3
js-sdsl: 4.2.0
js-yaml: 4.1.0
json-stable-stringify-without-jsonify: 1.0.1
@@ -877,6 +920,7 @@ packages:
/fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-glob/3.2.12:
resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
@@ -917,7 +961,7 @@ packages:
readable-web-to-node-stream: 3.0.2
strtok3: 7.0.0
token-types: 5.0.1
dev: false
dev: true
/fill-range/7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
@@ -1013,7 +1057,7 @@ packages:
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.2.12
ignore: 5.2.1
ignore: 5.2.4
merge2: 1.4.1
slash: 3.0.0
dev: true
@@ -1034,10 +1078,10 @@ packages:
/ieee754/1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
dev: true
/ignore/5.2.1:
resolution: {integrity: sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==}
/ignore/5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
dev: true
@@ -1063,6 +1107,7 @@ packages:
/inherits/2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
/is-binary-path/2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
@@ -1088,6 +1133,11 @@ packages:
engines: {node: '>=0.12.0'}
dev: true
/is-path-inside/3.0.3:
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
engines: {node: '>=8'}
dev: true
/is-stream/2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
@@ -1097,8 +1147,8 @@ packages:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
/iti/0.5.0:
resolution: {integrity: sha512-ZiwdEIhXAxyb6/+j2didONRa3q73y9h8oHMRXAIkXv7k851cm8H7fyZZyszr514TdzgoVQl7Z6hvsdTogjgc0w==}
/iti/0.6.0:
resolution: {integrity: sha512-JqujcnAIF3pmzitjbT3acc0LkordU6oHBDvWeT6a25wvEVBddFX3DFx/p6YBwGX1TTFsyLgVZtwhGOknthC96A==}
engines: {node: '>=12'}
dependencies:
utility-types: 3.10.0
@@ -1163,7 +1213,7 @@ packages:
/lodash.snakecase/4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
dev: false
dev: true
/lodash.sortby/4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
@@ -1171,7 +1221,7 @@ packages:
/lodash/4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
dev: true
/lru-cache/6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
@@ -1314,7 +1364,7 @@ packages:
/peek-readable/5.0.0:
resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==}
engines: {node: '>=14.16'}
dev: false
dev: true
/picomatch/2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
@@ -1347,8 +1397,8 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
/prettier/2.7.1:
resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==}
/prettier/2.8.3:
resolution: {integrity: sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==}
engines: {node: '>=10.13.0'}
hasBin: true
dev: true
@@ -1369,14 +1419,14 @@ packages:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
dev: false
dev: true
/readable-web-to-node-stream/3.0.2:
resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==}
engines: {node: '>=8'}
dependencies:
readable-stream: 3.6.0
dev: false
dev: true
/readdirp/3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
@@ -1412,8 +1462,8 @@ packages:
glob: 7.2.3
dev: true
/rollup/3.7.4:
resolution: {integrity: sha512-jN9rx3k5pfg9H9al0r0y1EYKSeiRANZRYX32SuNXAnKzh6cVyf4LZVto1KAuDnbHT03E1CpsgqDKaqQ8FZtgxw==}
/rollup/3.9.0:
resolution: {integrity: sha512-nGGylpmblyjTpF4lEUPgmOw6OVxRvnI6Iuuh6Lz4O/X66cVOX1XJSsqP1YamxQ+mPuFE7qJxLFDSCk8rNv5dDw==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
@@ -1426,15 +1476,15 @@ packages:
queue-microtask: 1.2.3
dev: true
/rxjs/7.7.0:
resolution: {integrity: sha512-SYVdj/JRf+CtYTBV+Nx1bIAl3iI7GL7kawWEmcNAdHVJMGacOPkuWBfd8QYSx3hDa6Y3Hoaqri4jYZ6jbUW5kw==}
/rxjs/7.8.0:
resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
dependencies:
tslib: 2.4.1
dev: false
/safe-buffer/5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
dev: true
/semver/7.3.8:
resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
@@ -1475,13 +1525,13 @@ packages:
/streamsearch/1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
dev: false
dev: true
/string_decoder/1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies:
safe-buffer: 5.2.1
dev: false
dev: true
/strip-ansi/6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
@@ -1506,7 +1556,7 @@ packages:
dependencies:
'@tokenizer/token': 0.3.0
peek-readable: 5.0.0
dev: false
dev: true
/sucrase/3.29.0:
resolution: {integrity: sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A==}
@@ -1558,7 +1608,7 @@ packages:
dependencies:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
dev: false
dev: true
/tr46/1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
@@ -1577,7 +1627,7 @@ packages:
/ts-mixer/6.0.2:
resolution: {integrity: sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A==}
dev: false
dev: true
/ts-pattern/4.0.6:
resolution: {integrity: sha512-sFHQYD4KoysBi7e7a2mzDPvRBeqA4w+vEyRE+P5MU9VLq8eEYxgKCgD9RNEAT+itGRWUTYN+hry94GDPLb1/Yw==}
@@ -1593,7 +1643,6 @@ packages:
/tslib/2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
dev: false
/tsup/6.5.0_typescript@4.9.4:
resolution: {integrity: sha512-36u82r7rYqRHFkD15R20Cd4ercPkbYmuvRkz3Q1LCm5BsiFNUgpo36zbjVhCOgvjyxNBWNKHsaD5Rl8SykfzNA==}
@@ -1621,7 +1670,7 @@ packages:
joycon: 3.1.1
postcss-load-config: 3.1.4
resolve-from: 5.0.0
rollup: 3.7.4
rollup: 3.9.0
source-map: 0.8.0-beta.0
sucrase: 3.29.0
tree-kill: 1.2.2
@@ -1664,7 +1713,7 @@ packages:
engines: {node: '>=12.18'}
dependencies:
busboy: 1.6.0
dev: false
dev: true
/uri-js/4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -1674,7 +1723,7 @@ packages:
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
dev: true
/utility-types/3.10.0:
resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==}
@@ -1721,7 +1770,7 @@ packages:
optional: true
utf-8-validate:
optional: true
dev: false
dev: true
/yallist/4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}

View File

@@ -1,24 +1,23 @@
import type { Observable } from 'rxjs';
import type { Logging } from './logging';
import { useContainerRaw } from '../dependencies/provider';
import util from 'util';
export interface ErrorHandling {
/**
* Number of times the process should throw an error until crashing and exiting
*/
keepAlive : number
keepAlive: number;
/**
* Utility function to crash
* @param error
*/
crash(error : Error) : never
crash(error: Error): never;
/**
* A function that is called on every crash. Updates keepAlive
* @param error
*/
updateAlive(error: Error): void
updateAlive(error: Error): void;
}
export class DefaultErrorHandling implements ErrorHandling {
@@ -35,10 +34,7 @@ export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
return (pload: unknown, caught: Observable<C>) => {
// This is done to fit the ErrorHandling contract
const err = pload instanceof Error ? pload : Error(util.format(pload));
if(crashHandler.keepAlive == 0) {
useContainerRaw()?.disposeAll().then(() => {
logging?.info({ message: 'Cleaning container and crashing' });
});
if (crashHandler.keepAlive == 0) {
crashHandler.crash(err);
}
//formatted payload
@@ -46,4 +42,4 @@ export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
crashHandler.updateAlive(err);
return caught;
};
}
}

View File

@@ -1,3 +1,3 @@
export { ErrorHandling, DefaultErrorHandling } from './errorHandling';
export { Logging, DefaultLogging } from './logging';
export { ModuleManager, DefaultModuleManager } from './moduleManager';
export { ModuleManager, DefaultModuleManager } from './moduleManager';

View File

@@ -1,10 +1,10 @@
import type { LogPayload } from '../../types/handler';
export interface Logging<T = unknown> {
error(payload : LogPayload<T>) : void;
warning(payload : LogPayload<T>) : void;
info(payload : LogPayload<T>) : void
debug(payload : LogPayload<T>) : void
error(payload: LogPayload<T>): void;
warning(payload: LogPayload<T>): void;
info(payload: LogPayload<T>): void;
debug(payload: LogPayload<T>): void;
}
export class DefaultLogging implements Logging {
@@ -24,5 +24,4 @@ export class DefaultLogging implements Logging {
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}

View File

@@ -1,16 +1,19 @@
import type { CommandModuleDefs } from '../../types/module';
import type { CommandType } from '../structures/enums';
import type { ModuleStore } from '../structures/moduleStore';
import type { CommandType, ModuleStore } from '../structures';
import type { Processed } from '../../types/handler';
export interface ModuleManager {
get<T extends CommandType>(strat : (ms: ModuleStore) => CommandModuleDefs[T] | undefined) : CommandModuleDefs[T] | undefined
set(strat: (ms: ModuleStore) => void) : void
get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
): CommandModuleDefs[T] | undefined;
set(strat: (ms: ModuleStore) => void): void;
}
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: ModuleStore) {}
get<T extends CommandType>(strat: (ms: ModuleStore) => CommandModuleDefs[T] | undefined) {
get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
) {
return strat(this.moduleStore);
}

View File

@@ -1,3 +1,2 @@
export {
useContainer
} from './provider';
export { single, transient, many } from './lifetimeFunctions';
export { useContainerRaw } from './provider';

View File

@@ -0,0 +1,50 @@
import { _const } from '../utilities/functions';
type NotFunction = string | number | boolean | null | undefined | bigint |
readonly any[] | { apply?: never, [k: string]: any } |
{ call?: never, [k: string]: any };
/**
* @deprecated
* @param cb
*/
export function single<T extends NotFunction>(cb: T) : () => T;
/**
* New signature
* @param cb
*/
export function single<T extends () => unknown>(cb: T) : T;
/**
* Please note that on intellij, the deprecation is for all signatures, which is unintended behavior (and
* very annoying).
* For future versions, ensure that single is being passed as a **callback!!**
* @param cb
*/
export function single<T>(cb: T) {
if(typeof cb === 'function') return cb;
return () => cb;
}
/**
* @deprecated
* @param cb
* Deprecated signature
*/
export function transient<T extends NotFunction>(cb: T) : () => () => T
export function transient<T extends () => () => unknown>(cb: T) : T;
/**
* Following iti's singleton and transient implementation,
* use transient if you want a new dependency every time your container getter is called
* @param cb
*/
export function transient<T>(cb: (() => () => T) | T) {
if(typeof cb !== 'function') return () => () => cb;
return cb;
}
/**
* @deprecated
* @param value
* Please use the transient function instead
*/
// prettier-ignore
export const many = <T>(value: T) => () => _const(value);

View File

@@ -1,83 +1,72 @@
import type { Container } from 'iti';
import { SernError } from '../structures/errors';
import { BehaviorSubject } from 'rxjs';
import * as assert from 'assert';
import type { Dependencies, MapDeps } from '../../types/handler';
import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler';
import SernEmitter from '../sernEmitter';
import { _const, ok } from '../utilities/functions';
import { DefaultErrorHandling, DefaultModuleManager, Logging } from '../contracts';
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts';
import { ModuleStore } from '../structures/moduleStore';
import { Ok, Result } from 'ts-results-es';
import { DefaultLogging } from '../contracts';
import { Result } from 'ts-results-es';
import { BehaviorSubject } from 'rxjs';
import { createContainer } from 'iti';
export const containerSubject = new BehaviorSubject<Container<Dependencies, Partial<Dependencies>> | null>(null);
export function composeRoot<T extends Dependencies>(root: Container<Partial<T>, Partial<Dependencies>>, exclusion: Set<keyof Dependencies>) {
const client = root.get('@sern/client');
assert.ok(client !== undefined, SernError.MissingRequired);
//A utility function checking if a dependency has been declared excluded
const excluded = (key: keyof Dependencies) => exclusion.has(key);
//Wraps a fetch to the container in a Result, deferring the action
const get = <T>(key : keyof Dependencies) => Result.wrap(() => root.get(key) as T);
const getOr = (key: keyof Dependencies, elseAction: () => unknown) => {
//Gets dependency but if an Err, map to a function that upserts.
const dep = get(key).mapErr(() => elseAction);
if(dep.err) {
//Defers upsert until final check here
return dep.val();
}
};
const xGetOr = (key: keyof Dependencies, action: () => unknown) => {
if(excluded(key)) {
get(key) //if dev created a dependency but excluded, deletes on root composition
.andThen(() => Ok(root.delete(key)))
.unwrapOr(ok());
} else {
getOr(key, action);
}
export const containerSubject = new BehaviorSubject(defaultContainer());
};
xGetOr('@sern/emitter', () => root.upsert({
'@sern/emitter' : _const(new SernEmitter())
})
);
//An "optional" dependency
xGetOr('@sern/logger', () => {
root.upsert({
'@sern/logger' : _const(new DefaultLogging())
});
}
);
xGetOr('@sern/store', () => root.upsert({
'@sern/store' : _const(new ModuleStore())
})
);
xGetOr('@sern/modules', () => root.upsert((ctx) => ({
'@sern/modules' : _const(new DefaultModuleManager(ctx['@sern/store'] as ModuleStore))
}))
);
xGetOr('@sern/errors', () => root.upsert({
'@sern/errors': _const(new DefaultErrorHandling())
})
);
//If logger exists, log info, else do nothing.
get<Logging>('@sern/logger')
.map((logger => logger.info({ message: 'All dependencies loaded successfully' })))
.unwrapOr(ok());
/**
* Given the user's conf, check for any excluded dependency keys.
* Then, call conf.build to get the rest of the users' dependencies.
* Finally, update the containerSubject with the new container state
* @param conf
*/
export function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//Get the current container. This should have no client or possible logger yet.
const currentContainer = containerSubject.getValue();
const excludeLogger = conf.exclude?.has('@sern/logger');
if(!excludeLogger) {
currentContainer.add({
'@sern/logger' : () => new DefaultLogging()
});
}
//Build the container based on the callback provided by the user
const container = conf.build(currentContainer);
//Check if the built container contains @sern/client or throw
// a runtime exception
Result
.wrap(() => container.get('@sern/client'))
.expect(SernError.MissingRequired);
if(!excludeLogger) {
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
}
//I'm sorry little one
containerSubject.next(container as any);
}
export function useContainer<T extends Dependencies>() {
const container = containerSubject.getValue()! as unknown as Container<T, {}>;
assert.ok(container !== null, 'useContainer was called before Sern#init');
//weird edge case, why can i not use _const here?
const container = containerSubject.getValue() as Container<T, {}>;
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => Result.wrap(() => container.get(key)).unwrapOr(undefined)) as MapDeps<T, V>;
}
/**
* Returns the underlying data structure holding all dependencies.
* Please be careful as this only gets the client's current state.
* Exposes some methods from iti
*/
export function useContainerRaw() {
return containerSubject.getValue();
export function useContainerRaw<T extends Dependencies>() {
return containerSubject.getValue() as Container<T, {}>;
}
/**
* Provides all the defaults for sern to function properly.
* The only user provided dependency needs to be @sern/client
*/
function defaultContainer() {
return createContainer()
.add({ '@sern/errors': () => new DefaultErrorHandling()})
.add({ '@sern/store' : () => new ModuleStore()})
.add(ctx => {
return {
'@sern/modules': () => new DefaultModuleManager(ctx['@sern/store'])
};
})
.add({ '@sern/emitter': () => new SernEmitter()}) as Container<Omit<Dependencies, '@sern/client' | '@sern/logger'>, {}>;
}

View File

@@ -1,170 +0,0 @@
import Context from '../structures/context';
import type { Payload, SlashOptions } from '../../types/handler';
import { arrAsync } from '../utilities/arrAsync';
import { controller } from '../sern';
import type {
ButtonInteraction,
ModalSubmitInteraction,
AutocompleteInteraction,
ChatInputCommandInteraction,
Interaction,
UserContextMenuCommandInteraction,
MessageContextMenuCommandInteraction,
} from 'discord.js';
import { SernError } from '../structures/errors';
import treeSearch from '../utilities/treeSearch';
import type {
BothCommand,
ButtonCommand, ContextMenuMsg,
ContextMenuUser,
ModalSubmitCommand,
StringSelectCommand,
SlashCommand, UserSelectCommand, ChannelSelectCommand, MentionableSelectCommand, RoleSelectCommand,
} from '../../types/module';
import type SernEmitter from '../sernEmitter';
import { EventEmitter } from 'events';
import type { DiscordEventCommand, ExternalEventCommand, SernEventCommand } from '../structures/events';
import * as assert from 'assert';
import { reducePlugins } from '../utilities/functions';
import { concatMap, from, fromEvent, map, of } from 'rxjs';
import type { MessageComponentInteraction } from 'discord.js';
export function applicationCommandDispatcher(interaction: Interaction) {
if (interaction.isAutocomplete()) {
return dispatchAutocomplete(interaction);
} else {
const ctx = Context.wrap(interaction as ChatInputCommandInteraction);
const args: ['slash', SlashOptions] = ['slash', ctx.interaction.options];
return (mod: BothCommand | SlashCommand) => ({
mod,
execute: () => mod.execute(ctx, args),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([ctx, args], controller)),
),
});
}
}
export function dispatchAutocomplete(interaction: AutocompleteInteraction) {
return (mod: BothCommand | SlashCommand) => {
const selectedOption = treeSearch(interaction, mod.options);
if (selectedOption !== undefined) {
return {
mod,
execute: () => selectedOption.command.execute(interaction),
eventPluginRes: arrAsync(
selectedOption.command.onEvent.map(e => e.execute(interaction, controller)),
),
};
}
throw Error(
SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
);
};
}
export function modalCommandDispatcher(interaction: ModalSubmitInteraction) {
return (mod: ModalSubmitCommand) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function buttonCommandDispatcher(interaction: ButtonInteraction) {
return (mod: ButtonCommand) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function selectMenuCommandDispatcher(interaction: MessageComponentInteraction) {
//safe casts because command type runtime check
return (mod: StringSelectCommand | UserSelectCommand | ChannelSelectCommand | MentionableSelectCommand | RoleSelectCommand) => ({
mod,
execute: () => mod.execute(interaction as never),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction as never], controller)),
),
});
}
export function ctxMenuUserDispatcher(interaction: UserContextMenuCommandInteraction) {
return (mod: ContextMenuUser) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function ctxMenuMsgDispatcher(interaction: MessageContextMenuCommandInteraction) {
return (mod: ContextMenuMsg) => ({
mod,
execute: () => mod.execute(interaction),
eventPluginRes: arrAsync(
mod.onEvent.map(plugs => plugs.execute([interaction], controller)),
),
});
}
export function sernEmitterDispatcher(e: SernEmitter) {
return(cmd: SernEventCommand & { name: string }) => ({
source: e,
cmd,
execute: fromEvent(e, cmd.name)
.pipe( map( event => ({
event,
executeEvent: of(event).pipe(concatMap(event =>
reducePlugins(from(
arrAsync(
cmd.onEvent.map(plug => plug.execute([event as Payload], controller))
))
)))
})))
});
}
export function discordEventDispatcher(e: EventEmitter) {
return (cmd: DiscordEventCommand & { name: string }) => ({
source: e,
cmd,
execute: fromEvent(e, cmd.name)
.pipe( map( event => ({
event,
executeEvent: of(event).pipe(concatMap( event =>
reducePlugins(from(
arrAsync(
// god forbid I use any!!!
cmd.onEvent.map(plug => plug.execute([event as any], controller))
))
)))
})))
});
}
export function externalEventDispatcher(e: (e:ExternalEventCommand) => unknown) {
return (cmd: ExternalEventCommand & { name: string}) => {
const external = e(cmd);
assert.ok(external instanceof EventEmitter, `${e} is not an EventEmitter`);
return {
source: external,
cmd,
execute: fromEvent(external, cmd.name)
.pipe(map(event => ({
event,
executeEvent : of(event).pipe(concatMap(event =>
reducePlugins(from(arrAsync(
cmd.onEvent.map(plug => plug.execute([event], controller))
)))
)),
})))
};
};
}

View File

@@ -0,0 +1,64 @@
import type { Processed } from '../../../types/handler';
import type { AutocompleteInteraction } from 'discord.js';
import { SernError } from '../../structures';
import treeSearch from '../../utilities/treeSearch';
import type { BothCommand, CommandModule, Module, SlashCommand } from '../../../types/module';
import { EventEmitter } from 'events';
import * as assert from 'assert';
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
import { callPlugin } from '../operators';
import { createResultResolver } from '../observableHandling';
export function dispatchCommand(module: Processed<CommandModule>, createArgs: () => unknown[]) {
const args = createArgs();
return {
module,
args,
};
}
/**
* Creates an observable from { source }
* @param module
* @param source
*/
export function eventDispatcher(module: Processed<Module>, source: unknown) {
assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
/**
* Sometimes fromEvent emits a single parameter, which is not an Array. This
* operator function flattens events into an array
* @param src
*/
const arrayify = pipe(
map(event => (Array.isArray(event) ? (event as unknown[]) : [event])),
map(args => ({ module, args })),
);
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
unknown[]
>({
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
onSuccess: ({ args }) => args,
});
const execute: OperatorFunction<unknown[], unknown> = pipe(
concatMap(async args => module.execute(...args)),
);
return fromEvent(source, module.name).pipe(arrayify, concatMap(createResult), execute);
}
export function dispatchAutocomplete(
module: Processed<BothCommand | SlashCommand>,
interaction: AutocompleteInteraction,
) {
const option = treeSearch(interaction, module.options);
if (option !== undefined) {
return {
module,
args: [interaction],
};
}
throw Error(
SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
);
}

View File

@@ -0,0 +1,2 @@
export * from './dispatchers';
export * from './provideArgs';

View File

@@ -0,0 +1,23 @@
import type { ChatInputCommandInteraction, Interaction, Message } from 'discord.js';
import { Context } from '../../structures';
import type { Args, SlashOptions } from '../../../types/handler';
/**
* function overloads to create an arguments list for Context
* @param wrap
* @param messageArgs
*/
export function contextArgs(
wrap: Message,
messageArgs?: string[],
): () => [Context, ['text', string[]]];
export function contextArgs(wrap: Interaction): () => [Context, ['slash', SlashOptions]];
export function contextArgs(wrap: Interaction | Message, messageArgs?: string[]) {
const ctx = Context.wrap(wrap as ChatInputCommandInteraction | Message);
const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.interaction.options];
return () => [ctx, args] as [Context, Args];
}
export function interactionArg<T extends Interaction>(interaction: T) {
return () => [interaction] as [T];
}

View File

@@ -4,6 +4,9 @@ import type { EventEmitter } from 'events';
import type SernEmitter from '../sernEmitter';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
/**
* why did I make this, definitely going to be changed in the future
*/
export abstract class EventsHandler<T> {
protected payloadSubject = new Subject<T>();
protected abstract discordEvent: Observable<unknown>;
@@ -13,14 +16,14 @@ export abstract class EventsHandler<T> {
protected logger?: Logging;
protected modules: ModuleManager;
protected constructor({ containerConfig }: Wrapper) {
const [
client,
emitter,
crash,
modules,
logger,
] = containerConfig.get('@sern/client', '@sern/emitter', '@sern/errors', '@sern/modules', '@sern/logger');
this.logger = logger as Logging|undefined;
const [client, emitter, crash, modules, logger] = containerConfig.get(
'@sern/client',
'@sern/emitter',
'@sern/errors',
'@sern/modules',
'@sern/logger',
);
this.logger = logger as Logging | undefined;
this.modules = modules as ModuleManager;
this.client = client as EventEmitter;
this.emitter = emitter as SernEmitter;

View File

@@ -1,33 +1,20 @@
import type { Interaction } from 'discord.js';
import { catchError, concatMap, from, fromEvent, map, Observable } from 'rxjs';
import { catchError, concatMap, finalize, fromEvent, map, Observable } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import { EventsHandler } from './eventsHandler';
import { SernError } from '../structures/errors';
import { CommandType, PayloadType } from '../structures/enums';
import { CommandType, SernError, type ModuleStore } from '../structures';
import { match, P } from 'ts-pattern';
import {
applicationCommandDispatcher,
buttonCommandDispatcher,
ctxMenuMsgDispatcher,
ctxMenuUserDispatcher,
modalCommandDispatcher,
selectMenuCommandDispatcher,
} from './dispatchers';
import type {
ButtonInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
MessageContextMenuCommandInteraction,
} from 'discord.js';
import { executeModule } from './observableHandling';
import { contextArgs, interactionArg, dispatchAutocomplete, dispatchCommand } from './dispatchers';
import { executeModule, makeModuleExecutor } from './observableHandling';
import type { CommandModule } from '../../types/module';
import { handleError } from '../contracts/errorHandling';
import type { ModuleStore } from '../structures/moduleStore';
import type { MessageComponentInteraction } from 'discord.js';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
export default class InteractionHandler extends EventsHandler<{
event: Interaction;
mod: CommandModule;
module: Processed<CommandModule>;
}> {
protected override discordEvent: Observable<Interaction>;
constructor(wrapper: Wrapper) {
@@ -37,83 +24,112 @@ export default class InteractionHandler extends EventsHandler<{
this.payloadSubject
.pipe(
map(this.processModules),
concatMap(({ mod, execute, eventPluginRes }) =>
from(eventPluginRes).pipe(map(res => ({ mod, res, execute }))) //resolve all the Results from event plugins
),
concatMap(payload => executeModule(wrapper, payload)),
map(this.createDispatcher),
makeModuleExecutor(module => {
this.emitter.emit(
'module.activate',
SernEmitter.failure(module, SernError.PluginFailure),
);
}),
concatMap(payload => executeModule(this.emitter, payload)),
catchError(handleError(this.crashHandler, this.logger)),
finalize(() => {
this.logger?.info({ message: 'interactionCreate stream closed or reached end of lifetime'});
useContainerRaw()
?.disposeAll()
.then(() => {
this.logger?.info({ message: 'Cleaning container and crashing' });
});
})
)
.subscribe();
}
override init() {
const get = (cb: (ms: ModuleStore) => CommandModule|undefined) => {
return this.modules.get(cb);
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return this.modules.get(cb);
};
/**
* Module retrieval:
* ModuleStores are mapped by Discord API values and modules mapped
* by customId or command name.
*/
this.discordEvent.subscribe({
next: event => {
if (event.isMessageComponent()) {
const mod = get(ms =>
ms.InteractionHandlers[event.componentType].get(event.customId));
this.setState({ event, mod });
} else if (event.isCommand() || event.isAutocomplete()) {
const mod = get(ms =>
ms.ApplicationCommands[event.commandType].get(event.commandName) ??
ms.BothCommands.get(event.commandName)
const module = get(ms =>
ms.InteractionHandlers[event.componentType].get(event.customId),
);
this.setState({ event, mod });
this.setState({ event, module });
} else if (event.isCommand() || event.isAutocomplete()) {
const module = get(
ms =>
/**
* try to fetch from ApplicationCommands, if nothing, try BothCommands
* map. If nothing again,this means a slash command
* exists on the API but not sern
*/
ms.ApplicationCommands[event.commandType].get(event.commandName) ??
ms.BothCommands.get(event.commandName),
);
this.setState({ event, module });
} else if (event.isModalSubmit()) {
const mod = get((ms) => ms.InteractionHandlers[5].get(event.customId));
this.setState({ event, mod });
const module = get(ms => ms.ModalSubmit.get(event.customId));
this.setState({ event, module });
} else {
throw Error('This interaction is not supported yet');
}
},
error: reason => {
this.emitter.emit('error', { type: PayloadType.Failure, reason });
this.emitter.emit('error', SernEmitter.failure(undefined, reason));
},
});
}
protected setState(state: { event: Interaction; mod: CommandModule | undefined }): void {
if (state.mod === undefined) {
this.emitter.emit('warning',{ type: PayloadType.Warning, reason: 'Found no module for this interaction' });
protected setState(state: { event: Interaction; module: CommandModule | undefined }): void {
if (state.module === undefined) {
this.emitter.emit(
'warning',
SernEmitter.warning('Found no module for this interaction'),
);
} else {
//if statement above checks already, safe cast
this.payloadSubject.next(state as { event: Interaction; mod: CommandModule });
this.payloadSubject.next(
state as { event: Interaction; module: Processed<CommandModule> },
);
}
}
protected processModules({ mod, event }: { event: Interaction; mod: CommandModule }) {
return match(mod)
.with(
{ type: P.union(CommandType.Slash, CommandType.Both) },
applicationCommandDispatcher(event),
)
.with(
{ type: CommandType.Modal },
modalCommandDispatcher(event as ModalSubmitInteraction),
)
.with({ type: CommandType.Button }, buttonCommandDispatcher(event as ButtonInteraction))
.with(
{ type: P.union(
CommandType.RoleSelect,
CommandType.StringSelect,
CommandType.UserSelect,
CommandType.MentionableSelect,
CommandType.ChannelSelect
) },
selectMenuCommandDispatcher(event as MessageComponentInteraction),
)
.with(
{ type: CommandType.CtxUser },
ctxMenuUserDispatcher(event as UserContextMenuCommandInteraction),
)
.with(
{ type: CommandType.CtxMsg },
ctxMenuMsgDispatcher(event as MessageContextMenuCommandInteraction),
)
.otherwise(() => this.crashHandler.crash(Error(SernError.MismatchModule)));
protected createDispatcher({
module,
event,
}: {
event: Interaction;
module: Processed<CommandModule>;
}) {
return (
match(module)
.with({ type: CommandType.Text }, () =>
this.crashHandler.crash(Error(SernError.MismatchEvent)),
)
//P.union = either CommandType.Slash or CommandType.Both
.with({ type: P.union(CommandType.Slash, CommandType.Both) }, module => {
if (event.isAutocomplete()) {
/**
* Autocomplete is a special case that
* must be handled separately, since it's
* too different from regular command modules
*/
return dispatchAutocomplete(module, event);
} else {
return dispatchCommand(module, contextArgs(event));
}
})
/**
* Every other command module takes a one argument parameter, its corresponding interaction
* this makes this usage safe
*/
.otherwise(mod => dispatchCommand(mod, interactionArg(event)))
);
}
}

View File

@@ -1,39 +1,44 @@
import { EventsHandler } from './eventsHandler';
import { catchError, concatMap, from, fromEvent, map, Observable, of, switchMap } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of } from 'rxjs';
import { type Wrapper, type ModuleStore, SernError } from '../structures';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, isOneOfCorrectModules } from './observableHandling';
import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
import { fmt } from '../utilities/messageHelpers';
import Context from '../structures/context';
import { CommandType, PayloadType } from '../structures/enums';
import { arrAsync } from '../utilities/arrAsync';
import { controller } from '../sern';
import type { CommandModule, TextCommand } from '../../types/module';
import type { CommandModule, Module, TextCommand } from '../../types/module';
import { handleError } from '../contracts/errorHandling';
import type { ModuleStore } from '../structures/moduleStore';
import { contextArgs, dispatchCommand } from './dispatchers';
import SernEmitter from '../sernEmitter';
import type { Processed } from '../../types/handler';
import { useContainerRaw } from '../dependencies';
export default class MessageHandler extends EventsHandler<{
ctx: Context;
args: ['text', string[]];
mod: TextCommand;
module: Processed<Module>;
args: unknown[];
}> {
protected discordEvent: Observable<Message>;
public constructor(protected wrapper: Wrapper) {
super(wrapper);
this.discordEvent = <Observable<Message>>fromEvent(this.client, 'messageCreate');
this.init();
this.payloadSubject
.pipe(
switchMap(({ mod, ctx, args }) => {
const res = arrAsync(
mod.onEvent.map(ep => ep.execute([ctx, args], controller)),
makeModuleExecutor(module => {
this.emitter.emit(
'module.activate',
SernEmitter.failure(module, SernError.PluginFailure),
);
const execute = () => mod.execute(ctx, args);
//resolves the promise and re-emits it back into source
return from(res).pipe(map(res => ({ mod, execute, res })));
}),
concatMap(payload => executeModule(wrapper, payload)),
concatMap(payload => executeModule(this.emitter, payload)),
catchError(handleError(this.crashHandler, this.logger)),
finalize(() => {
this.logger?.info({ message: 'messageCreate stream closed or reached end of lifetime'});
useContainerRaw()
?.disposeAll()
.then(() => {
this.logger?.info({ message: 'Cleaning container and crashing' });
});
})
)
.subscribe();
}
@@ -41,38 +46,37 @@ export default class MessageHandler extends EventsHandler<{
protected init(): void {
if (this.wrapper.defaultPrefix === undefined) return; //for now, just ignore if prefix doesn't exist
const { defaultPrefix } = this.wrapper;
const get = (cb: (ms: ModuleStore) => CommandModule | undefined) => {
return this.modules.get(cb);
const get = (cb: (ms: ModuleStore) => Processed<CommandModule> | undefined) => {
return this.modules.get(cb);
};
this.discordEvent
.pipe(
ignoreNonBot(this.wrapper.defaultPrefix),
map(message => {
//This concatMap checks if module is undefined, and if it is, do not continue.
// Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
concatMap(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix);
return {
ctx: Context.wrap(message),
args: <['text', string[]]>['text', rest],
mod: get(ms =>
ms.TextCommands.get(prefix) ??
ms.BothCommands.get(prefix)
),
const module = get(
ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix),
);
if (module === undefined) {
return EMPTY;
}
const payload = {
args: contextArgs(message, rest),
module,
};
return of(payload);
}),
concatMap(element =>
of(element.mod).pipe(
isOneOfCorrectModules(CommandType.Text),
map(mod => ({ ...element, mod })),
),
),
map(({ args, module }) => dispatchCommand(module as Processed<TextCommand>, args)),
)
.subscribe({
next: value => this.setState(value),
error: reason =>
this.emitter.emit('error', { type: PayloadType.Failure, reason }),
error: reason => this.emitter.emit('error', SernEmitter.failure(reason)),
});
}
protected setState(state: { ctx: Context; args: ['text', string[]]; mod: TextCommand }) {
protected setState(state: { module: Processed<Module>; args: unknown[] }) {
this.payloadSubject.next(state);
}
}

View File

@@ -1,20 +1,21 @@
import type { Message } from 'discord.js';
import { concatMap, from, map, Observable, of, switchMap, tap, throwError, toArray } from 'rxjs';
import { SernError } from '../structures/errors';
import type { Awaitable, Message } from 'discord.js';
import { concatMap, EMPTY, from, Observable, of, pipe, tap, throwError } from 'rxjs';
import type { SernError } from '../structures';
import { Result } from 'ts-results-es';
import type { CommandType } from '../structures/enums';
import type Wrapper from '../structures/wrapper';
import { PayloadType, PluginType } from '../structures/enums';
import type { CommandModule, CommandModuleDefs, AnyModule } from '../../types/module';
import { _const } from '../utilities/functions';
import type SernEmitter from '../sernEmitter';
import type { DefinedCommandModule, DefinedEventModule } from '../../types/handler';
import type { Awaitable } from 'discord.js';
import { processCommandPlugins } from './userDefinedEventsHandling';
import type { AnyModule, CommandModule, EventModule, Module } from '../../types/module';
import { _const as i } from '../utilities/functions';
import SernEmitter from '../sernEmitter';
import { callPlugin, everyPluginOk, filterMapTo } from './operators';
import type { Processed } from '../../types/handler';
import type { VoidResult } from '../../types/plugin';
export function ignoreNonBot(prefix: string) {
return (src: Observable<Message>) =>
new Observable<Message>(subscriber => {
/**
* Ignores messages from any person / bot except itself
* @param prefix
*/
export function ignoreNonBot<T extends Message>(prefix: string) {
return (src: Observable<T>) =>
new Observable<T>(subscriber => {
return src.subscribe({
next(m) {
const messageFromHumanAndHasPrefix =
@@ -26,19 +27,19 @@ export function ignoreNonBot(prefix: string) {
subscriber.next(m);
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
}
/**
* If the current value in Result stream is an error, calls callback.
* This also extracts the Ok value from Result
* @param cb
* @returns Observable<{ module: T; absPath: string }>
*/
export function errTap<T extends AnyModule>(cb: (err: SernError) => void) {
return (src: Observable<Result<{ mod: T; absPath: string }, SernError>>) =>
new Observable<{ mod: T; absPath: string }>(subscriber => {
return (src: Observable<Result<{ module: T; absPath: string }, SernError>>) =>
new Observable<{ module: T; absPath: string }>(subscriber => {
return src.subscribe({
next(value) {
if (value.err) {
@@ -47,97 +48,108 @@ export function errTap<T extends AnyModule>(cb: (err: SernError) => void) {
subscriber.next(value.val);
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
}
//POG
export function isOneOfCorrectModules<T extends readonly CommandType[]>(...inputs: [...T]) {
return (src: Observable<CommandModule | undefined>) => {
return new Observable<CommandModuleDefs[T[number]]>(subscriber => {
return src.subscribe({
next(mod) {
if (mod === undefined) {
return throwError(_const(SernError.UndefinedModule));
}
if (inputs.some(type => (mod.type & type) !== 0)) {
subscriber.next(mod as CommandModuleDefs[T[number]]);
} else {
return throwError(_const(SernError.MismatchModule));
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
};
}
/**
* Wraps the task in a Result as a try / catch.
* if the task is ok, an event is emitted and the stream becomes empty
* if the task is an error, throw an error down the stream which will be handled by catchError
* @param emitter reference to SernEmitter that will emit a successful execution of module
* @param module the module that will be executed with task
* @param task the deferred execution which will be called
*/
export function executeModule(
wrapper: Wrapper,
payload: {
mod: CommandModule;
execute: () => unknown;
res: Result<void, void>[];
emitter: SernEmitter,
{
module,
task,
}: {
module: Processed<Module>;
task: () => Awaitable<unknown>;
},
) {
const emitter = wrapper.containerConfig.get('@sern/emitter')[0] as SernEmitter;
if (payload.res.every(el => el.ok)) {
const executeFn = Result.wrapAsync<unknown, Error | string>(() => Promise.resolve(payload.execute()));
return from(executeFn).pipe(
concatMap(res => {
if (res.err) {
return throwError(() => ({
type: PayloadType.Failure,
reason: res.val,
module: payload.mod,
}));
}
return of(res.val).pipe(
tap(() =>
emitter.emit('module.activate', {
type: PayloadType.Success,
module: payload.mod,
}),
),
);
}),
);
} else {
emitter.emit('module.activate', {
type: PayloadType.Failure,
module: payload.mod,
reason: SernError.PluginFailure,
});
return of(undefined);
}
}
export function resolvePlugins({ mod, cmdPluginRes }: {
mod: DefinedCommandModule | DefinedEventModule;
cmdPluginRes: {
execute: Awaitable<Result<void, void>>;
type: PluginType.Command;
}[];
}) {
if (mod.plugins.length === 0) {
return of({ mod, pluginRes: [] });
}
// modules with no event plugins are ignored in the previous
return from(cmdPluginRes).pipe(
switchMap(pl =>
from(pl.execute).pipe(
map(execute => ({ ...pl, execute })),
toArray(),
),
),
map(pluginRes => ({ mod, pluginRes })),
return of(module).pipe(
//converting the task into a promise so rxjs can resolve the Awaitable properly
concatMap(() => Result.wrapAsync(async () => task())),
concatMap(result => {
if (result.ok) {
emitter.emit('module.activate', SernEmitter.success(module));
return EMPTY;
} else {
return throwError(i(SernEmitter.failure(module, result.val)));
}
}),
);
}
export function processPlugins(payload: { mod: DefinedCommandModule | DefinedEventModule; absPath: string }) {
const cmdPluginRes = processCommandPlugins(payload);
return of({ mod: payload.mod, cmdPluginRes });
}
/**
* A higher order function that
* - creates a stream of {@link VoidResult} { config.createStream }
* - any failures results to { config.onFailure } being called
* - if all results are ok, the stream is converted to { config.onSuccess }
* emit config.onSuccess Observable
* @param config
* @returns receiver function for flattening a stream of data
*/
export function createResultResolver<
T extends Processed<Module>,
Args extends { module: T; [key: string]: unknown },
Output,
>(config: {
onFailure?: (module: T) => unknown;
onSuccess: (args: Args) => Output;
createStream: (args: Args) => Observable<VoidResult>;
}) {
return (args: Args) => {
const task$ = config.createStream(args);
return task$.pipe(
tap(result => {
if (result.err) {
config.onFailure?.(args.module);
}
}),
everyPluginOk(),
filterMapTo(() => config.onSuccess(args)),
);
};
}
/**
* Calls a module's init plugins and checks for Err. If so, call { onFailure } and
* ignore the module
*/
export function scanModule<
T extends Processed<CommandModule | EventModule>,
Args extends { module: T; absPath: string },
>(config: { onFailure?: (module: T) => unknown; onSuccess: (module: Args) => T }) {
return pipe(
concatMap(
createResultResolver({
createStream: args => from(args.module.plugins).pipe(callPlugin(args)),
...config,
}),
),
);
}
/**
* Creates an executable task ( execute the command ) if all control plugins are successful
* @param onFailure emits a failure response to the SernEmitter
*/
export function makeModuleExecutor<
M extends Processed<Module>,
Args extends { module: M; args: unknown[] },
>(onFailure: (m: M) => unknown) {
const onSuccess = ({ args, module }: Args) => ({ task: () => module.execute(...args), module });
return pipe(
concatMap(
createResultResolver({
onFailure,
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
onSuccess,
}),
),
);
}

View File

@@ -0,0 +1,70 @@
/**
* This file holds sern's rxjs operators used for processing data.
* Each function should be modular and testable, not bound to discord / sern
* and independent of each other
*/
import { concatMap, defaultIfEmpty, EMPTY, every, map, of, OperatorFunction, pipe } from 'rxjs';
import type { AnyModule } from '../../../types/module';
import { nameOrFilename } from '../../utilities/functions';
import type { PluginResult, VoidResult } from '../../../types/plugin';
import { guayin } from '../../plugins';
import { controller } from '../../sern';
/**
* if {src} is true, mapTo V, else ignore
* @param item
*/
export function filterMapTo<V>(item: () => V): OperatorFunction<boolean, V> {
return pipe(concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY)));
}
/**
* Calls any plugin with {args}.
* @param args if an array, its spread and plugin called.
*/
export function callPlugin(args: unknown): OperatorFunction<
{
execute: (...args: unknown[]) => PluginResult;
},
VoidResult
> {
return pipe(
concatMap(async plugin => {
const isNewPlugin = Reflect.has(plugin, guayin);
if(isNewPlugin) {
if (Array.isArray(args)) {
return plugin.execute(...args);
}
return plugin.execute(args);
} else {
return plugin.execute(args, controller);
}
}),
);
}
/**
* operator function that fill the defaults for a module
*/
export function defineAllFields<T extends AnyModule>() {
const fillFields = ({ absPath, module }: { absPath: string; module: T }) => ({
absPath,
module: {
name: nameOrFilename(module.name, absPath),
description: module.description ?? '...',
...module,
},
});
return pipe(map(fillFields));
}
/**
* Checks if the stream of results is all ok.
*/
export function everyPluginOk(): OperatorFunction<VoidResult, boolean> {
return pipe(
every(result => result.ok),
defaultIfEmpty(true),
);
}

View File

@@ -1,134 +1,119 @@
import { EventsHandler } from './eventsHandler';
import type Wrapper from '../structures/wrapper';
import { concatMap, fromEvent, Observable, map, take } from 'rxjs';
import { concatMap, fromEvent, type Observable, take } from 'rxjs';
import * as Files from '../utilities/readFile';
import { errTap, processPlugins, resolvePlugins } from './observableHandling';
import { CommandType, PayloadType } from '../structures/enums';
import { SernError } from '../structures/errors';
import { errTap, scanModule } from './observableHandling';
import { CommandType, SernError, type ModuleStore } from '../structures';
import { match } from 'ts-pattern';
import { Result } from 'ts-results-es';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../../types/module';
import type { DefinedCommandModule, DefinedEventModule } from '../../types/handler';
import type { Processed } from '../../types/handler';
import type { ModuleManager } from '../contracts';
import type { ModuleStore } from '../structures/moduleStore';
import { _const, err, nameOrFilename, ok } from '../utilities/functions';
import { _const, err, ok } from '../utilities/functions';
import { defineAllFields } from './operators';
import SernEmitter from '../sernEmitter';
export default class ReadyHandler extends EventsHandler<{
mod: DefinedCommandModule;
module: Processed<CommandModule>;
absPath: string;
}> {
protected discordEvent!: Observable<{ mod: CommandModule; absPath: string }>;
protected discordEvent!: Observable<{ module: CommandModule; absPath: string }>;
constructor(wrapper: Wrapper) {
super(wrapper);
const ready$ = fromEvent(this.client, 'ready').pipe(take(1));
this.discordEvent = ready$.pipe(
concatMap(() =>
Files.buildData<CommandModule>(wrapper.commands).pipe(
errTap(reason =>
this.emitter.emit('module.register', {
type: PayloadType.Failure,
module: undefined,
reason,
}),
),
errTap(reason => {
this.emitter.emit(
'module.register',
SernEmitter.failure(undefined, reason),
);
}),
),
),
);
this.init();
this.payloadSubject
.pipe(
concatMap(processPlugins),
concatMap(resolvePlugins),
).subscribe(payload => {
const allPluginsSuccessful = payload.pluginRes.every(({ execute }) => execute.ok);
if (allPluginsSuccessful) {
const res = registerModule(this.modules, payload.mod);
if (res.err) {
this.crashHandler.crash(Error(SernError.InvalidModuleType));
}
this.emitter.emit('module.register', {
type: PayloadType.Success,
module: payload.mod,
});
} else {
this.emitter.emit('module.register', {
type: PayloadType.Failure,
module: payload.mod,
reason: SernError.PluginFailure,
});
scanModule({
onFailure: module => {
this.emitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onSuccess: ({ module }) => {
this.emitter.emit('module.register', SernEmitter.success(module));
return module;
},
}),
)
.subscribe(module => {
const res = registerModule(this.modules, module as Processed<CommandModule>);
if (res.err) {
this.crashHandler.crash(Error(SernError.InvalidModuleType));
}
});
}
private static intoDefinedModule({ absPath, mod }: { absPath: string; mod: CommandModule }): {
absPath: string;
mod: DefinedCommandModule;
} {
return {
absPath,
mod: {
name: nameOrFilename(mod.name, absPath),
description: mod?.description ?? '...',
...mod,
},
};
}
protected init() {
this.discordEvent.pipe(map(ReadyHandler.intoDefinedModule)).subscribe({
this.discordEvent.pipe(defineAllFields()).subscribe({
next: value => this.setState(value),
complete: () => this.payloadSubject.unsubscribe(),
});
}
protected setState(state: { absPath: string; mod: DefinedCommandModule }): void {
protected setState(state: { absPath: string; module: Processed<CommandModule> }): void {
this.payloadSubject.next(state);
}
}
function registerModule(manager: ModuleManager, mod: DefinedCommandModule | DefinedEventModule): Result<void, void> {
function registerModule<T extends Processed<CommandModule>>(
manager: ModuleManager,
mod: T,
): Result<void, void> {
const name = mod.name;
const insert = (cb: (ms: ModuleStore) => void) => {
const set = Result.wrap(_const(manager.set(cb)));
return set.ok ? ok() : err();
};
return match<DefinedCommandModule | DefinedEventModule>(mod)
return match(mod as Processed<CommandModule>)
.with({ type: CommandType.Text }, mod => {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.TextCommands.set(name, mod));
})
.with({ type: CommandType.Slash }, mod =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod))
insert(ms => ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod)),
)
.with({ type: CommandType.Both }, mod => {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert( ms => ms.BothCommands.set(name, mod));
return insert(ms => ms.BothCommands.set(name, mod));
})
.with({ type: CommandType.CtxUser }, mod =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod))
insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod)),
)
.with({ type: CommandType.CtxMsg }, mod =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod))
insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod)),
)
.with({ type: CommandType.Button }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod))
insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod)),
)
.with({ type: CommandType.StringSelect }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod))
insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod)),
)
.with( { type: CommandType.MentionableSelect }, mod =>
insert (ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod))
.with({ type: CommandType.MentionableSelect }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod)),
)
.with( { type: CommandType.ChannelSelect }, mod =>
insert ( ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod))
.with({ type: CommandType.ChannelSelect }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod)),
)
.with( { type: CommandType.UserSelect }, mod =>
insert ( ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod))
.with({ type: CommandType.UserSelect }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod)),
)
.with( { type: CommandType.RoleSelect}, mod =>
insert ( ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod))
)
.with({ type: CommandType.Modal }, mod =>
insert(ms => ms.ModalSubmit.set(name, mod))
.with({ type: CommandType.RoleSelect }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod)),
)
.with({ type: CommandType.Modal }, mod => insert(ms => ms.ModalSubmit.set(name, mod)))
.otherwise(err);
}

View File

@@ -1,108 +1,72 @@
import { catchError, concatMap, filter, from, iif, map, of, tap, toArray } from 'rxjs';
import { catchError, finalize, map, tap } from 'rxjs';
import { buildData } from '../utilities/readFile';
import { controller } from '../sern';
import type { DefinedCommandModule, DefinedEventModule, Dependencies } from '../../types/handler';
import { PayloadType, PluginType } from '../structures/enums';
import type Wrapper from '../structures/wrapper';
import { isDiscordEvent, isExternalEvent, isSernEvent } from '../utilities/predicates';
import { errTap, processPlugins, resolvePlugins } from './observableHandling';
import type { AnyModule, EventModule } from '../../types/module';
import type { Dependencies, Processed } from '../../types/handler';
import { errTap, scanModule } from './observableHandling';
import type { CommandModule, EventModule } from '../../types/module';
import type { EventEmitter } from 'events';
import type SernEmitter from '../sernEmitter';
import { nameOrFilename, reducePlugins } from '../utilities/functions';
import SernEmitter from '../sernEmitter';
import { match } from 'ts-pattern';
import { discordEventDispatcher, externalEventDispatcher, sernEmitterDispatcher } from './dispatchers';
import type { ErrorHandling, Logging } from '../contracts';
import { SernError } from '../structures/errors';
import { SernError, EventType, type Wrapper } from '../structures';
import { eventDispatcher } from './dispatchers';
import { handleError } from '../contracts/errorHandling';
import type { Awaitable } from 'discord.js';
import type { Result } from 'ts-results-es';
/**
* Utility function to process command plugins for all Modules
* @param payload
*/
export function processCommandPlugins<T extends DefinedCommandModule | DefinedEventModule>(
payload: { mod: T; absPath: string },
): {type: PluginType.Command, execute: Awaitable<Result<void,void>>}[] {
return payload.mod.plugins.map(plug => ({
type: plug.type,
execute: plug.execute(payload as any, controller),
}));
}
import { defineAllFields } from './operators';
import { useContainerRaw } from '../dependencies';
export function processEvents({ containerConfig, events }: Wrapper) {
const [
client,
error,
sernEmitter,
logging
] = containerConfig.get('@sern/client', '@sern/errors', '@sern/emitter', '@sern/logger') as [EventEmitter, ErrorHandling, SernEmitter, Logging?];
const [client, errorHandling, sernEmitter, logger] = containerConfig.get(
'@sern/client',
'@sern/errors',
'@sern/emitter',
'@sern/logger',
) as [EventEmitter, ErrorHandling, SernEmitter, Logging?];
const lazy = (k: string) => containerConfig.get(k as keyof Dependencies)[0];
const eventStream$ = eventObservable$(events!, sernEmitter);
const emitSuccess$ = (mod: AnyModule) =>
of({ type: PayloadType.Failure, module: mod, reason: SernError.PluginFailure })
.pipe(tap( it => sernEmitter.emit('module.register', it)));
const emitFailure$ = (mod: AnyModule) =>
of({ type: PayloadType.Success, module: mod, } as const)
.pipe(tap(it => sernEmitter.emit('module.register', it)));
const eventCreation$ = eventStream$.pipe(
map(({ mod, absPath }) => ({
mod : {
name: nameOrFilename(mod.name, absPath),
...mod,
} as DefinedEventModule,
absPath
})),
concatMap(processPlugins),
concatMap(resolvePlugins),
//Reduces pluginRes (generated from above) into a single boolean
concatMap(({pluginRes, mod}) => from(pluginRes)
.pipe(
map(pl => pl.execute),
toArray(),
reducePlugins,
map(success => ({ success, mod }))
)),
concatMap(({ success, mod }) =>
iif(() => success,
emitFailure$(mod),
emitSuccess$(mod)
).pipe(
filter(res => res.type === PayloadType.Success),
map(() => mod)
)
),
defineAllFields(),
scanModule({
onFailure: module => sernEmitter.emit('module.register', SernEmitter.success(module)),
onSuccess: ({ module }) => {
sernEmitter.emit(
'module.register',
SernEmitter.failure(module, SernError.PluginFailure),
);
return module;
},
}),
);
eventCreation$.subscribe(e => {
const payload = match(e)
.when(isSernEvent, sernEmitterDispatcher(sernEmitter))
.when(isDiscordEvent, discordEventDispatcher(client))
.when(isExternalEvent, externalEventDispatcher(e => lazy(e.emitter)))
.otherwise(() => error.crash(Error(SernError.InvalidModuleType)));
payload
.execute
.pipe(
concatMap(({event, executeEvent}) =>
executeEvent
.pipe( tap(success => {
if(success)
payload.cmd.execute(event as never);
}), catchError(handleError(error, logging)))
),
).subscribe();
});
const intoDispatcher = (e: Processed<EventModule | CommandModule>) =>
match(e)
.with({ type: EventType.Sern }, m => eventDispatcher(m, sernEmitter))
.with({ type: EventType.Discord }, m => eventDispatcher(m, client))
.with({ type: EventType.External }, m => eventDispatcher(m, lazy(m.emitter)))
.otherwise(() => errorHandling.crash(Error(SernError.InvalidModuleType)));
eventCreation$
.pipe(
map(intoDispatcher),
/**
* Where all events are turned on
*/
tap(dispatcher => dispatcher.subscribe()),
catchError(handleError(errorHandling, logger)),
finalize(() => {
logger?.info({ message: 'an event module reached end of lifetime'});
useContainerRaw()
?.disposeAll()
.then(() => {
logger?.info({ message: 'Cleaning container and crashing' });
});
})
)
.subscribe();
}
function eventObservable$(events: string, emitter: SernEmitter) {
return buildData<EventModule>(events)
.pipe(
errTap(reason =>
emitter.emit('module.register', {
type: PayloadType.Failure,
module: undefined,
reason,
}),
),
);
return buildData<EventModule>(events).pipe(
errTap(reason => {
emitter.emit('module.register', SernEmitter.failure(undefined, reason));
}),
);
}

116
src/handler/plugins/args.ts Normal file
View File

@@ -0,0 +1,116 @@
import type { CommandType } from '../structures/enums';
import type { PluginType } from '../structures/enums';
import type { ClientEvents } from 'discord.js';
import type {
BothCommand,
ButtonCommand,
ChannelSelectCommand,
ContextMenuUser,
DiscordEventCommand,
ExternalEventCommand,
MentionableSelectCommand,
ModalSubmitCommand,
RoleSelectCommand,
SernEventCommand,
SlashCommand,
StringSelectCommand,
TextCommand,
UserSelectCommand,
ContextMenuMsg, Module,
} from '../../types/module';
import type { Args, Payload, Processed, SlashOptions } from '../../types/handler';
import type Context from '../structures/context';
import type { MessageContextMenuCommandInteraction } from 'discord.js';
import type {
ButtonInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import type {
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
ModalSubmitInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { EventType } from '../structures/enums';
type CommandArgsMatrix = {
[CommandType.Text]: {
[PluginType.Control]: [Context, ['text', string[]]];
[PluginType.Init]: [InitArgs<Processed<TextCommand>>];
};
[CommandType.Slash]: {
[PluginType.Control]: [Context, ['slash', /* library coupled */ SlashOptions]];
[PluginType.Init]: [InitArgs<Processed<SlashCommand>>];
};
[CommandType.Both]: {
[PluginType.Control]: [Context, Args];
[PluginType.Init]: [InitArgs<Processed<BothCommand>>];
};
[CommandType.CtxMsg]: {
[PluginType.Control]: [/* library coupled */ MessageContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<ContextMenuMsg>>];
};
[CommandType.CtxUser]: {
[PluginType.Control]: [/* library coupled */ UserContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<ContextMenuUser>>];
};
[CommandType.Button]: {
[PluginType.Control]: [/* library coupled */ ButtonInteraction];
[PluginType.Init]: [InitArgs<Processed<ButtonCommand>>];
};
[CommandType.StringSelect]: {
[PluginType.Control]: [/* library coupled */ StringSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<StringSelectCommand>>];
};
[CommandType.RoleSelect]: {
[PluginType.Control]: [/* library coupled */ RoleSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<RoleSelectCommand>>];
};
[CommandType.ChannelSelect]: {
[PluginType.Control]: [/* library coupled */ ChannelSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<ChannelSelectCommand>>];
};
[CommandType.MentionableSelect]: {
[PluginType.Control]: [/* library coupled */ MentionableSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<MentionableSelectCommand>>];
};
[CommandType.UserSelect]: {
[PluginType.Control]: [/* library coupled */ UserSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<UserSelectCommand>>];
};
[CommandType.Modal]: {
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
};
};
type EventArgsMatrix = {
[EventType.Discord]: {
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
};
[EventType.Sern]: {
[PluginType.Control]: [Payload];
[PluginType.Init]: [InitArgs<Processed<SernEventCommand>>];
};
[EventType.External]: {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
};
export interface InitArgs<T extends Processed<Module>> {
module: T;
absPath: string;
}
export type CommandArgs<
I extends CommandType = CommandType,
J extends PluginType = PluginType,
> = CommandArgsMatrix[I][J];
export type EventArgs<
I extends EventType = EventType,
J extends PluginType = PluginType,
> = EventArgsMatrix[I][J];

View File

@@ -0,0 +1,51 @@
import { CommandType, EventType, PluginType } from '../structures/enums';
import type { Plugin, PluginResult } from '../../types/plugin';
import type { CommandArgs, EventArgs } from './args';
import type { ClientEvents } from 'discord.js';
export const guayin = Symbol('twice<3');
export function makePlugin<V extends unknown[]>(
type: PluginType,
execute: (...args: any[]) => any,
): Plugin<V> {
return {
type,
execute,
[guayin]: undefined
} as Plugin<V>;
}
export function EventInitPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
export function CommandInitPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
export function EventControlPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @Experimental
* A specialized function for creating control plugins with discord.js ClientEvents.
* Will probably be moved one day!
*/
export function DiscordEventControlPlugin<T extends keyof ClientEvents>(
name: T,
execute: (...args: ClientEvents[T]) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}

View File

@@ -0,0 +1,2 @@
export { EventArgs, InitArgs, CommandArgs } from './args';
export * from './createPlugin';

View File

@@ -1,147 +0,0 @@
/*
* Plugins can be inserted on all commands and are emitted
*
* 1. On ready event, where all commands are loaded.
* 2. On corresponding observable (when command triggers)
*
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cool-downs, permissions, etc.
* Plugins are reminiscent of middleware in express.
*/
import type { AutocompleteInteraction, Awaitable, ClientEvents } from 'discord.js';
import type { Result, Ok, Err } from 'ts-results-es';
import type { CommandType, SernEventsMapping } from '../../index';
import { EventType, PluginType } from '../../index';
import type { CommandModuleDefs, EventModuleDefs } from '../../types/module';
import type {
DiscordEventCommand,
ExternalEventCommand,
SernEventCommand,
} from '../structures/events';
export interface Controller {
next: () => Ok<void>;
stop: () => Err<void>;
}
export interface Plugin {
/** @deprecated will be removed in the next update */
name?: string;
/** @deprecated will be removed in the next update */
description?: string;
type: PluginType
}
export interface CommandPlugin<T extends keyof CommandModuleDefs = keyof CommandModuleDefs> extends Plugin {
type: PluginType.Command;
execute: (
payload: {
mod: CommandModuleDefs[T] & { name : string; description : string };
absPath: string;
},
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface DiscordEmitterPlugin extends Plugin {
type: PluginType.Command;
execute: (
payload: { mod: DiscordEventCommand & { name: string }; absPath: string },
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface ExternalEmitterPlugin extends Plugin {
type: PluginType.Command;
execute: (
payload: { mod: ExternalEventCommand & { name : string }; absPath: string } ,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface SernEmitterPlugin extends Plugin {
type: PluginType.Command;
execute: (
payload: { mod : SernEventCommand & { name : string }; absPath: string },
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface AutocompletePlugin extends Plugin {
type: PluginType.Event;
execute: (
autocmp: AutocompleteInteraction,
controlller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface EventPlugin<K extends keyof CommandModuleDefs = keyof CommandModuleDefs> extends Plugin {
type: PluginType.Event;
execute: (
event: Parameters<CommandModuleDefs[K]['execute']>,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface SernEventPlugin<T extends keyof SernEventsMapping = keyof SernEventsMapping> extends Plugin {
name?: T;
type: PluginType.Event;
execute: (
args: SernEventsMapping[T],
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export interface ExternalEventPlugin extends Plugin {
type: PluginType.Event;
execute: (args: unknown[], controller: Controller) => Awaitable<Result<void, void>>;
}
export interface DiscordEventPlugin<T extends keyof ClientEvents = keyof ClientEvents> extends Plugin {
name?: T;
type: PluginType.Event;
execute: (args: ClientEvents[T], controller: Controller) => Awaitable<Result<void, void>>;
}
export type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
};
/**
* Event Module Event Plugins
*/
export type EventModuleEventPluginDefs = {
[EventType.Discord]: DiscordEventPlugin;
[EventType.Sern]: SernEventPlugin;
[EventType.External]: ExternalEventPlugin;
};
/**
* Event Module Command Plugins
*/
export type EventModuleCommandPluginDefs = {
[EventType.Discord]: DiscordEmitterPlugin;
[EventType.Sern]: SernEmitterPlugin;
[EventType.External]: ExternalEmitterPlugin;
};
export type EventModulePlugin<T extends EventType> =
| EventModuleEventPluginDefs[T]
| EventModuleCommandPluginDefs[T];
export type CommandModulePlugin<T extends CommandType> = EventPlugin<T> | CommandPlugin<T>;
/**
* User inputs this type. Sern processes behind the scenes for better usage
*/
export type InputCommandModule = {
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: CommandModulePlugin<T>[] };
}[CommandType];
export type InputEventModule = {
[T in EventType]: EventModulesNoPlugins[T] & { plugins?: EventModulePlugin<T>[] };
}[EventType];

View File

@@ -1,24 +1,24 @@
import type Wrapper from './structures/wrapper';
import { processEvents } from './events/userDefinedEventsHandling';
import { CommandType, EventType, PluginType } from './structures/enums';
import type {
Plugin,
CommandPlugin,
EventModuleCommandPluginDefs,
EventModuleEventPluginDefs,
EventPlugin,
InputCommandModule,
InputEventModule,
} from './plugins/plugin';
import type { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../types/plugin';
import InteractionHandler from './events/interactionHandler';
import ReadyHandler from './events/readyHandler';
import MessageHandler from './events/messageHandler';
import type { CommandModule, CommandModuleDefs, EventModule, EventModuleDefs } from '../types/module';
import { Container, createContainer } from 'iti';
import type { Dependencies, OptionalDependencies } from '../types/handler';
import { composeRoot, containerSubject, useContainer } from './dependencies/provider';
import type {
CommandModule,
CommandModuleDefs,
EventModule,
EventModuleDefs,
InputCommand,
InputEvent,
} from '../types/module';
import type { Dependencies, DependencyConfiguration } from '../types/handler';
import { composeRoot, useContainer } from './dependencies/provider';
import type { Logging } from './contracts';
import { err, ok, partition } from './utilities/functions';
import type { Awaitable, ClientEvents } from 'discord.js';
/**
*
* @param wrapper Options to pass into sern.
@@ -46,7 +46,7 @@ export function init(wrapper: Wrapper) {
new MessageHandler(wrapper);
new InteractionHandler(wrapper);
const endTime = performance.now();
logger?.info({ message: `sern : ${(endTime-startTime).toFixed(2)} ms` });
logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
}
/**
@@ -61,8 +61,11 @@ export const controller = {
* The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommandModule): CommandModule {
const [onEvent, plugins] = partition(mod.plugins ?? [], el => (el as Plugin).type === PluginType.Event);
export function commandModule(mod: InputCommand): CommandModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
onEvent,
@@ -73,37 +76,56 @@ export function commandModule(mod: InputCommandModule): CommandModule {
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule(mod: InputEventModule): EventModule {
const [onEvent, plugins] = partition(mod.plugins ?? [], el => (el as Plugin).type === PluginType.Event);
return {
export function eventModule(mod: InputEvent): EventModule {
const [onEvent, plugins] = partition(
mod.plugins ?? [],
el => (el as Plugin).type === PluginType.Control,
);
return {
...mod,
onEvent,
plugins,
} as EventModule;
}
/**
* Create event modules from discord.js client events,
* This is an {@link eventModule} for discord events,
* where typings can be very bad.
* @param mod
*/
export function discordEvent<T extends keyof ClientEvents>(mod: {
name: T;
plugins?: AnyEventPlugin[];
execute: (...args: ClientEvents[T]) => Awaitable<unknown>;
}) {
return eventModule({ type: EventType.Discord, ...mod });
}
/**
* @param conf a configuration for creating your project dependencies
*/
export function makeDependencies<T extends Dependencies>(conf: {
exclude?: Set<OptionalDependencies>,
build: (root: Container<Record<string,any>, {}>) => Container<Partial<T>, T>,
}) {
const container = conf.build(createContainer());
composeRoot(container, conf.exclude ?? new Set());
containerSubject.next(container as unknown as Container<Dependencies, {}>);
export function makeDependencies<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//Until there are more optional dependencies, just check if the logger exists
composeRoot(conf);
return useContainer<T>();
}
/**
* @Experimental
* Will be refactored / changed in future
*/
export abstract class CommandExecutable<Type extends CommandType> {
abstract type: Type;
plugins: CommandPlugin<Type>[] = [];
onEvent: EventPlugin<Type>[] = [];
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: CommandModuleDefs[Type]['execute'];
}
/**@Experimental
* Will be refactored in future
*/
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
plugins: EventModuleCommandPluginDefs[Type][] = [];
onEvent: EventModuleEventPluginDefs[Type][] = [];
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute: EventModuleDefs[Type]['execute'];
}

View File

@@ -1,5 +1,7 @@
import { EventEmitter } from 'events';
import type { SernEventsMapping } from '../types/handler';
import type { Payload, SernEventsMapping } from '../types/handler';
import { PayloadType } from './structures';
import type { Module } from '../types/module';
class SernEmitter extends EventEmitter {
/**
@@ -35,6 +37,49 @@ class SernEmitter extends EventEmitter {
): boolean {
return super.emit(eventName, ...args);
}
private static payload<T extends Payload>(
type: PayloadType,
module?: Module,
reason?: unknown,
) {
return { type, module, reason } as T;
}
/**
* Creates a compliant SernEmitter failure payload
* @param module
* @param reason
*/
static failure(module?: Module, reason?: unknown) {
//The generic cast Payload & { type : PayloadType.* } coerces the type to be a failure payload
// same goes to the other methods below
return SernEmitter.payload<Payload & { type: PayloadType.Failure }>(
PayloadType.Failure,
module,
reason,
);
}
/**
* Creates a compliant SernEmitter module success payload
* @param module
*/
static success(module: Module) {
return SernEmitter.payload<Payload & { type: PayloadType.Success }>(
PayloadType.Success,
module,
);
}
/**
* Creates a compliant SernEmitter module warning payload
* @param reason
*/
static warning(reason: unknown) {
return SernEmitter.payload<Payload & { type: PayloadType.Warning }>(
PayloadType.Warning,
undefined,
reason,
);
}
}
export default SernEmitter;

View File

@@ -11,7 +11,6 @@ import { Result as Either, Ok as Left, Err as Right } from 'ts-results-es';
import type { ReplyOptions } from '../../types/handler';
import { SernError } from './errors';
function safeUnwrap<T>(res: Either<T, T>) {
return res.val;
}
@@ -20,16 +19,14 @@ function safeUnwrap<T>(res: Either<T, T>) {
* Message and ChatInputCommandInteraction
*/
export default class Context {
private constructor(
private ctx: Either<Message, ChatInputCommandInteraction>,
) {}
private constructor(private ctx: Either<Message, ChatInputCommandInteraction>) {}
/**
* Getting the Message object. Crashes if module type is
* CommandType.Slash or the event fired in a Both command was
* ChatInputCommandInteraction
*/
public get message() {
public get message() {
return this.ctx.expect(SernError.MismatchEvent);
}
/**
@@ -42,71 +39,45 @@ export default class Context {
}
public get id(): Snowflake {
return safeUnwrap(
this.ctx
.map(m => m.id)
.mapErr(i => i.id));
return this.ctx.val.id;
}
public get channel() {
return safeUnwrap(this.ctx
.map(m => m.channel)
.mapErr(i => i.channel));
return this.ctx.val.channel;
}
/**
* If context is holding a message, message.author
* else, interaction.user
*/
public get user(): User {
return safeUnwrap(this.ctx
.map(m => m.author)
.mapErr(i => i.user)
);
return safeUnwrap(this.ctx.map(m => m.author).mapErr(i => i.user));
}
public get createdTimestamp(): number {
return safeUnwrap(this.ctx
.map(m => m.createdTimestamp)
.mapErr(i => i.createdTimestamp)
);
return this.ctx.val.createdTimestamp;
}
public get guild() {
return safeUnwrap(this.ctx
.map(m => m.guild)
.mapErr(i => i.guild)
);
return this.ctx.val.guild;
}
public get guildId() {
return safeUnwrap(this.ctx
.map(m => m.guildId)
.mapErr(i => i.guildId)
);
return this.ctx.val.guildId;
}
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
public get member() {
return safeUnwrap(this.ctx
.map(m => m.member)
.mapErr(i => i.member)
);
return this.ctx.val.member;
}
public get client(): Client {
return safeUnwrap(this.ctx
.map(m => m.client)
.mapErr(i => i.client)
);
return this.ctx.val.client;
}
public get inGuild(): boolean {
return safeUnwrap(this.ctx
.map(m=>m.inGuild())
.mapErr(i => i.inGuild())
);
return this.ctx.val.inGuild();
}
public isMessage() {
return this.ctx.map(() => true).unwrapOr(false);
@@ -124,9 +95,12 @@ export default class Context {
}
public reply(content: ReplyOptions) {
return safeUnwrap(this.ctx
.map( m => m.reply(content as string | MessageReplyOptions))
.mapErr(i => i.reply(content as string | InteractionReplyOptions).then(() => i.fetchReply()))
return safeUnwrap(
this.ctx
.map(m => m.reply(content as string | MessageReplyOptions))
.mapErr(i =>
i.reply(content as string | InteractionReplyOptions).then(() => i.fetchReply()),
),
);
}
}

View File

@@ -51,8 +51,8 @@ export enum CommandType {
*/
ChannelSelect = 256,
MentionableSelect = 512,
RoleSelect= 1024,
UserSelect= 2048
RoleSelect = 1024,
UserSelect = 2048,
}
/**
@@ -102,13 +102,23 @@ export enum EventType {
*/
export enum PluginType {
/**
* The PluginType for CommandPlugins
* The PluginType for InitPlugins
*/
Init = 1,
/**
* @deprecated
* Use PluginType.Init instead
*/
Command = 1,
/**
* The PluginType for EventPlugins
* @deprecated
* Use PluginType.Control instead
*/
Event = 2,
/**
* The PluginType for EventPlugins
*/
Control = 2,
}
/**
* @enum { string }

View File

@@ -34,5 +34,5 @@ export enum SernError {
/**
* Required Dependency not found
*/
MissingRequired = `@sern/client is required but was not found`
MissingRequired = `@sern/client is required but was not found`,
}

View File

@@ -1,38 +0,0 @@
import type { SernEventsMapping } from '../../types/handler';
import type {
DiscordEmitterPlugin,
DiscordEventPlugin,
ExternalEmitterPlugin,
ExternalEventPlugin,
SernEmitterPlugin,
SernEventPlugin,
} from '../plugins/plugin';
import type { Awaitable, ClientEvents } from 'discord.js';
import type { EventType } from './enums';
import type { Module } from '../../types/module';
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping> extends Module {
name?: T;
type: EventType.Sern;
onEvent: SernEventPlugin[];
plugins: SernEmitterPlugin[];
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
}
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents> extends Module {
name?: T;
type: EventType.Discord;
onEvent: DiscordEventPlugin[];
plugins: DiscordEmitterPlugin[];
execute(...args: ClientEvents[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string
emitter: string;
type: EventType.External;
onEvent: ExternalEventPlugin[];
plugins: ExternalEmitterPlugin[];
execute(...args: unknown[]): Awaitable<unknown>;
}

View File

@@ -1,10 +1,6 @@
import Context from './context';
import type Wrapper from './wrapper';
import { ModuleStore } from './moduleStore';
export * from './errors';
export * from './enums';
export {
Context,
Wrapper,
ModuleStore
};
export { Context, Wrapper, ModuleStore };

View File

@@ -1,26 +1,26 @@
import type { CommandModule } from '../../types/module';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { Processed } from '../../types/handler';
/**
* Storing all command modules
* This dependency is usually injected into ModuleManager
*/
export class ModuleStore {
readonly BothCommands = new Map<string, CommandModule>();
readonly ApplicationCommands = {
[ApplicationCommandType.User]: new Map<string, CommandModule>(),
[ApplicationCommandType.Message]: new Map<string, CommandModule>(),
[ApplicationCommandType.ChatInput]: new Map<string, CommandModule>(),
readonly BothCommands = new Map<string, Processed<CommandModule>>();
readonly ApplicationCommands = {
[ApplicationCommandType.User]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.Message]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.ChatInput]: new Map<string, Processed<CommandModule>>(),
};
readonly ModalSubmit = new Map<string, CommandModule>();
readonly TextCommands = new Map<string, CommandModule>();
readonly ModalSubmit = new Map<string, Processed<CommandModule>>();
readonly TextCommands = new Map<string, Processed<CommandModule>>();
readonly InteractionHandlers = {
[ComponentType.Button]: new Map<string, CommandModule>(),
[ComponentType.StringSelect]: new Map<string, CommandModule>(),
[ComponentType.ChannelSelect] : new Map<string, CommandModule>(),
[ComponentType.MentionableSelect] : new Map<string, CommandModule>(),
[ComponentType.RoleSelect] : new Map<string, CommandModule>(),
[ComponentType.UserSelect] : new Map<string, CommandModule>(),
[ComponentType.Button]: new Map<string, Processed<CommandModule>>(),
[ComponentType.StringSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.ChannelSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.MentionableSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.RoleSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.UserSelect]: new Map<string, Processed<CommandModule>>(),
};
}

View File

@@ -8,8 +8,8 @@ interface Wrapper {
readonly defaultPrefix?: string;
readonly commands: string;
readonly events?: string;
readonly containerConfig : {
readonly containerConfig: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
}
};
}
export default Wrapper;

View File

@@ -1,9 +0,0 @@
import type { Awaitable } from 'discord.js';
export async function arrAsync<T>(promiseLike: Awaitable<T>[]): Promise<T[]> {
const arr: T[] = [];
for await (const el of promiseLike) {
arr.push(el);
}
return arr;
}

View File

@@ -1,20 +1,18 @@
import * as Files from './readFile';
import { basename } from 'path';
import { Err, Ok, Result } from 'ts-results-es';
import { Observable, of, switchMap } from 'rxjs';
import { Err, Ok } from 'ts-results-es';
/**
* A function that returns whatever value is provided.
* Used for singleton in iti
* Warning: this evaluates { @param value }. It does not defer a value.
* @param value
*/
// prettier-ignore
export const _const = <T>(value: T) => () => value;
/**
* A function that returns another function
* Used for transient in iti
* @param value
*
* @param modName
* @param absPath
*/
export const transient = <T>( value : T) => () => _const(value);
export function nameOrFilename(modName: string | undefined, absPath: string) {
return modName ?? Files.fmtFileName(basename(absPath));
}
@@ -23,20 +21,15 @@ export function nameOrFilename(modName: string | undefined, absPath: string) {
export const ok = _const(Ok.EMPTY);
export const err = _const(Err.EMPTY);
export function partition<T, V>(arr: (T & V)[], condition: (e: (T & V)) => boolean) : [T[], V[]] {
const t : T[] = [];
const v : V[] = [];
for(const el of arr) {
if(condition(el)) {
export function partition<T, V>(arr: (T & V)[], condition: (e: T & V) => boolean): [T[], V[]] {
const t: T[] = [];
const v: V[] = [];
for (const el of arr) {
if (condition(el)) {
t.push(el as T);
} else {
v.push(el as V);
}
}
return [ t, v ];
return [t, v];
}
export function reducePlugins(src: Observable<Result<void, void>[]>) : Observable<boolean> {
return src.pipe(switchMap(s => of(s.every((a) => a.ok))));
}

View File

@@ -1,23 +0,0 @@
import type { DiscordEventCommand, ExternalEventCommand, SernEventCommand } from '../structures/events';
import { CommandModule, EventType } from '../..';
import type { AnyModule, CommandModuleDefs, EventModule } from '../../types/module';
export function correctModuleType<T extends keyof CommandModuleDefs>(
plug: AnyModule | undefined,
type: T,
): plug is CommandModuleDefs[T] {
// Another way to check if type is equivalent,
// It will check based on flag system instead
return plug !== undefined && (plug.type & type) !== 0;
}
export function isDiscordEvent(el: EventModule | CommandModule): el is DiscordEventCommand {
return el.type === EventType.Discord;
}
export function isSernEvent(el: EventModule | CommandModule): el is SernEventCommand {
return el.type === EventType.Sern;
}
export function isExternalEvent(el: EventModule | CommandModule): el is ExternalEventCommand {
return el.type === EventType.External && 'emitter' in el;
}

View File

@@ -4,7 +4,6 @@ import { type Observable, from, concatAll } from 'rxjs';
import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
try {
@@ -23,7 +22,8 @@ function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
/**
*
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
@@ -31,7 +31,7 @@ export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
export function buildData<T>(commandDir: string): Observable<
Result<
{
mod: T;
module: T;
absPath: string;
},
SernError
@@ -41,20 +41,20 @@ export function buildData<T>(commandDir: string): Observable<
return from(
Promise.all(
commands.map(async absPath => {
let mod: T | undefined;
let module: T | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
mod = require(absPath).default;
module = require(absPath).default;
} catch {
mod = (await import(`file:///` + absPath)).default;
module = (await import(`file:///` + absPath)).default;
}
if (mod === undefined) {
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
mod = new (mod as unknown as new () => T)();
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ mod, absPath });
return Ok({ module, absPath });
}),
),
).pipe(concatAll());

View File

@@ -1,6 +1,11 @@
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
/**
* Uses an iterative DFS to check if an autocomplete node exists
* @param iAutocomplete
* @param options
*/
export default function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
@@ -13,30 +18,30 @@ export default function treeSearch(
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand:
{
for (const option of cur.options ?? []) {
_options.push(option);
}
}
break;
case ApplicationCommandOptionType.SubcommandGroup:
{
for (const command of cur.options ?? []) {
_options.push(command);
}
}
break;
default:
{
if (cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
if (cur.name === choice.name && cur.autocomplete) {
autocompleteData = cur;
{
for (const option of cur.options ?? []) {
_options.push(option);
}
}
break;
case ApplicationCommandOptionType.SubcommandGroup:
{
for (const command of cur.options ?? []) {
_options.push(command);
}
}
break;
default:
{
if (cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
if (cur.name === choice.name && cur.autocomplete) {
autocompleteData = cur;
}
}
}
}
break;
}
}
return autocompleteData;
}
}

View File

@@ -1,11 +1,18 @@
import SernEmitter from './handler/sernEmitter';
export { eventModule, commandModule, EventExecutable, CommandExecutable } from './handler/sern';
export {
eventModule,
commandModule,
EventExecutable,
CommandExecutable,
controller,
discordEvent,
} from './handler/sern';
export * as Sern from './handler/sern';
export * from './types/handler';
export * from './types/module';
export * from './handler/structures/structxports';
export * from './handler/plugins/plugin';
export * from './handler/contracts/index';
export * from './types/plugin';
export * from './handler/structures';
export * from './handler/plugins';
export * from './handler/contracts';
export { SernEmitter };
export { _const as single, transient as many } from './handler/utilities/functions';
export { useContainerRaw } from './handler/dependencies/provider';
export * from './handler/dependencies';

View File

@@ -7,6 +7,7 @@ import type { UnpackFunction } from 'iti';
import type { ErrorHandling, Logging, ModuleManager } from '../handler/contracts';
import type { ModuleStore } from '../handler/structures/moduleStore';
import type SernEmitter from '../handler/sernEmitter';
import type { Container } from 'iti';
// Thanks to @kelsny
export type ParseType<T> = {
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
@@ -20,38 +21,49 @@ export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' |
* After modules are transformed, name and description are given default values if none
* are provided to Module. This type represents that transformation
*/
export type DefinedCommandModule = CommandModule & { name: string; description: string };
export type DefinedEventModule = EventModule & { name: string };
export type AnyDefinedModule = DefinedCommandModule | DefinedEventModule
export type AnyDefinedModule = Processed<CommandModule | EventModule>;
export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string};
| { type: PayloadType.Warning; reason: string };
export type SernEventsMapping = {
'module.register': [Payload];
'module.activate': [Payload];
'error': [Payload];
'warning': [Payload];
error: [Payload];
warning: [Payload];
};
export type LogPayload<T = unknown> = { message: T }
export type Singleton<T> = () => T
export type LogPayload<T = unknown> = { message: T };
export type Singleton<T> = () => T;
export type Transient<T> = () => () => T;
export interface Dependencies {
'@sern/client': Singleton<EventEmitter>;
'@sern/logger'?: Singleton<Logging>;
'@sern/emitter': Singleton<SernEmitter>;
'@sern/store' : Singleton<ModuleStore>;
'@sern/modules' : Singleton<ModuleManager>;
'@sern/store': Singleton<ModuleStore>;
'@sern/modules': Singleton<ModuleManager>;
'@sern/errors': Singleton<ErrorHandling>;
}
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;
export type MapDeps<
Deps extends Dependencies,
T extends readonly unknown[]
> = T extends [infer First extends keyof Deps, ...infer Rest extends readonly unknown[]]
? [ UnpackFunction<Deps[First]>, ...(MapDeps<Deps, Rest> extends [never] ? [] : MapDeps<Deps,Rest>)] : [never]
export type ReplyOptions =
| string
| Omit<InteractionReplyOptions, 'fetchReply'>
| MessageReplyOptions;
//prettier-ignore
export type MapDeps<Deps extends Dependencies, T extends readonly unknown[]> = T extends [
infer First extends keyof Deps,
...infer Rest extends readonly unknown[],
]
? [
UnpackFunction<Deps[First]>,
...(MapDeps<Deps, Rest> extends [never] ? [] : MapDeps<Deps, Rest>),
]
: [never];
//Basically, '@sern/client' | '@sern/store' | '@sern/modules' | '@sern/error' | '@sern/emitter' will be provided defaults, and you can exclude the rest
export type OptionalDependencies = '@sern/logger';
export type OptionalDependencies = '@sern/logger';
export type Processed<T> = T & { name: string; description: string };
export type Deprecated<Message extends string> = [never, Message]
export interface DependencyConfiguration<T extends Dependencies> {
exclude?: Set<OptionalDependencies>;
build: (root: Container<Omit<Dependencies, '@sern/client'>, {}>) => Container<T, {}>
}

View File

@@ -18,123 +18,120 @@ import type {
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction
StringSelectMenuInteraction,
} from 'discord.js';
import type {
DiscordEventCommand,
ExternalEventCommand,
SernEventCommand,
} from '../handler/structures/events';
import { CommandType } from '../handler/structures/enums';
import type { Args, SlashOptions } from './handler';
import type Context from '../handler/structures/context';
import type { AutocompletePlugin, CommandPlugin, EventPlugin } from '../handler/plugins/plugin';
import type { InitPlugin, ControlPlugin } from './plugin';
import { EventType } from '../handler/structures/enums';
import type { UserSelectMenuInteraction } from 'discord.js';
import type { AnyCommandPlugin, AnyEventPlugin } from './plugin';
import type { SernEventsMapping } from './handler';
import type { ClientEvents } from 'discord.js';
export interface Module {
type?: CommandType | EventType;
type: CommandType | EventType;
name?: string;
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
execute: (...args: any[]) => any
execute: (...args: any[]) => Awaitable<any>;
}
export interface TextCommand extends Module {
type: CommandType.Text;
onEvent: EventPlugin<CommandType.Text>[];
plugins: CommandPlugin[];
alias?: string[];
execute: (ctx: Context, args: ['text', string[]]) => Awaitable<unknown>;
}
export interface SlashCommand extends Module {
type: CommandType.Slash;
onEvent: EventPlugin<CommandType.Slash>[];
plugins: CommandPlugin[];
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: ['slash', SlashOptions]) => Awaitable<unknown>;
}
export interface BothCommand extends Module {
type: CommandType.Both;
onEvent: EventPlugin<CommandType.Both>[];
plugins: CommandPlugin[];
alias?: string[];
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: Args) => Awaitable<unknown>;
}
export interface ContextMenuUser extends Module {
type: CommandType.CtxUser;
onEvent: EventPlugin<CommandType.CtxUser>[];
plugins: CommandPlugin[];
execute: (ctx: UserContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ContextMenuMsg extends Module {
type: CommandType.CtxMsg;
onEvent: EventPlugin<CommandType.CtxMsg>[];
plugins: CommandPlugin[];
execute: (ctx: MessageContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ButtonCommand extends Module {
type: CommandType.Button;
onEvent: EventPlugin<CommandType.Button>[];
plugins: CommandPlugin[];
execute: (ctx: ButtonInteraction) => Awaitable<unknown>;
}
export interface StringSelectCommand extends Module {
type: CommandType.StringSelect;
onEvent: EventPlugin<CommandType.StringSelect>[];
plugins: CommandPlugin[];
execute: (ctx: StringSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect;
onEvent: EventPlugin<CommandType.ChannelSelect>[];
plugins: CommandPlugin[];
execute: (ctx: ChannelSelectMenuInteraction) => Awaitable<unknown>;
}
export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect;
onEvent: EventPlugin<CommandType.RoleSelect>[];
plugins: CommandPlugin[];
execute: (ctx: RoleSelectMenuInteraction) => Awaitable<unknown>;
}
export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect;
onEvent: EventPlugin<CommandType.MentionableSelect>[];
plugins: CommandPlugin[];
execute: (ctx: MentionableSelectMenuInteraction) => Awaitable<unknown>;
}
export interface UserSelectCommand extends Module {
type: CommandType.UserSelect;
onEvent: EventPlugin<CommandType.UserSelect>[];
plugins: CommandPlugin[];
execute: (ctx: UserSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ModalSubmitCommand extends Module {
type: CommandType.Modal;
onEvent: EventPlugin<CommandType.Modal>[];
plugins: CommandPlugin[];
execute: (ctx: ModalSubmitInteraction) => Awaitable<unknown>;
}
export interface AutocompleteCommand extends Module {
name?: never;
description?: never;
type?: never;
onEvent: AutocompletePlugin[];
export interface AutocompleteCommand
extends Omit<Module, 'name' | 'type' | 'plugins' | 'description'> {
onEvent: ControlPlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
}
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
extends Module {
name?: T;
type: EventType.Sern;
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
}
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents>
extends Module {
name?: T;
type: EventType.Discord;
execute(...args: ClientEvents[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string;
emitter: string;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
@@ -162,10 +159,10 @@ export type CommandModuleDefs = {
[CommandType.CtxUser]: ContextMenuUser;
[CommandType.Button]: ButtonCommand;
[CommandType.StringSelect]: StringSelectCommand;
[CommandType.RoleSelect] : RoleSelectCommand;
[CommandType.ChannelSelect] : ChannelSelectCommand;
[CommandType.MentionableSelect] : MentionableSelectCommand;
[CommandType.UserSelect] : UserSelectCommand;
[CommandType.RoleSelect]: RoleSelectCommand;
[CommandType.ChannelSelect]: ChannelSelectCommand;
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
};
@@ -175,7 +172,8 @@ export type EventModuleDefs = {
[EventType.External]: ExternalEventCommand;
};
export interface SernAutocompleteData extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
autocomplete: true;
type:
| ApplicationCommandOptionType.String
@@ -184,6 +182,21 @@ export interface SernAutocompleteData extends Omit<BaseApplicationCommandOptions
command: AutocompleteCommand;
}
export type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type InputEvent = {
[T in EventType]: EventModulesNoPlugins[T] & { plugins?: AnyEventPlugin[] };
}[EventType];
export type InputCommand = {
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: AnyCommandPlugin[] };
}[CommandType];
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
@@ -212,4 +225,4 @@ export type SernOptionsData<U extends ApplicationCommandOptionData = Application
? SernSubCommandData
: U extends ApplicationCommandSubGroupData
? SernSubCommandGroupData
: BaseOptions;
: BaseOptions;

71
src/types/plugin.ts Normal file
View File

@@ -0,0 +1,71 @@
/*
* Plugins can be inserted on all commands and are emitted
*
* 1. On ready event, where all commands are loaded.
* 2. On corresponding observable (when command triggers)
*
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cool-downs, permissions, etc.
* Plugins are reminiscent of middleware in express.
*/
import type { Awaitable } from 'discord.js';
import type { Err, Ok, Result } from 'ts-results-es';
import type { PluginType } from '../handler/structures/enums';
import type { CommandModule, EventModule } from './module';
import type { CommandArgs, InitArgs } from '../handler/plugins';
import type { Deprecated, Processed } from './handler';
import type { CommandType } from '../handler/structures/enums';
export type PluginResult = Awaitable<VoidResult>;
export type VoidResult = Result<void, void>;
export interface Controller {
next: () => Ok<void>
stop: () => Err<void>
}
export interface Plugin<Args extends any[] = any[]> {
type: PluginType;
execute: (...args: Args) => PluginResult;
}
export interface InitPlugin<Args extends any[] = any[]> {
type: PluginType.Init;
execute: (...args: Args) => PluginResult;
}
export interface ControlPlugin<Args extends any[] = any[]> {
type: PluginType.Control;
execute: (...args: Args) => PluginResult;
}
export type AnyCommandPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<CommandModule>>]>;
export type AnyEventPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<EventModule>>]>;
/**
* @deprecated
* Use the newer helper functions and import { controller } from '@sern/handler'
*/
export interface CommandPlugin<T extends CommandType = CommandType> {
name?: string;
description?: string;
type: PluginType.Command;
execute: (m: InitArgs<Processed<CommandModule>>, controller?: Deprecated<'Please import controller instead'>) => PluginResult;
}
/**
* @deprecated
* Use the newer helper functions
*/
export interface EventPlugin<T extends CommandType> {
name?: string;
description?: string;
type: PluginType.Event;
execute: (args : CommandArgs<T, PluginType.Event>, controller?: Controller) => PluginResult
}
export type DiscordEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type ExternalEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type SernEmitterPlugin = Deprecated<'Please view alternatives: '>;
export type AutocompletePlugin = Deprecated<'Please view alternatives: '>;
export type SernEventPlugin = Deprecated<'Please view alternatives: '>;
export type ExternalEventPlugin = Deprecated<'Please view alternatives: '>;
export type DiscordEventPlugin = Deprecated<'Please view alternatives: '>;

View File

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"baseUrl": ".",
"resolveJsonModule": true,
"moduleResolution": "node",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/secrets.json", "src"]
}

View File

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"baseUrl": ".",
"resolveJsonModule": true,
"moduleResolution": "node",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/secrets.json", "src"]
}

View File

@@ -4,7 +4,7 @@ const shared = {
external: ['discord.js'],
platform: 'node',
clean: true,
sourcemap: false
sourcemap: false,
};
export default defineConfig([
{
@@ -19,7 +19,7 @@ export default defineConfig([
js: '.mjs',
};
},
...shared
...shared,
},
{
format: 'cjs',
@@ -33,6 +33,6 @@ export default defineConfig([
js: '.cjs',
};
},
...shared
...shared,
},
]);