Compare commits

..

66 Commits

Author SHA1 Message Date
Evo
4812106ff0 chore: rebased api update branch (#65)
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
Co-authored-by: xxDeveloper <77380166+Murtatrxx@users.noreply.github.com>
2022-06-28 12:32:11 -05:00
Jacob Nguyen
d6bfd45782 fix: load external event emitters correctly 2022-06-28 11:09:37 -05:00
Jacob Nguyen
237179f590 fix: forgot to add required type property in comment 2022-06-28 10:40:23 -05:00
Jacob Nguyen
ea8cb4ddd1 docs(sern.ts): add comments for addExternal 2022-06-28 10:38:09 -05:00
Jacob Nguyen
9fbd890db8 build: bump version to 1.1.0-beta 2022-06-28 10:30:52 -05:00
Jacob Nguyen
db217f5533 build: delete test.yml, change .prettierignore 2022-06-28 10:18:08 -05:00
Jacob Nguyen
cf3bb412d5 build: remove 'tests' exclude 2022-06-28 10:00:15 -05:00
Jacob Nguyen
84ff217d7f feat: add absPath into commandPlugins! 2022-06-27 15:54:13 -05:00
Jacob Nguyen
4f520b2e5a style: add comment 2022-06-27 15:49:09 -05:00
Jacob Nguyen
4c2a7e8c90 style: eslint 2022-06-27 14:24:54 -05:00
Jacob Nguyen
9320d4395a feat: change from client -> wrapper 2022-06-27 13:47:09 -05:00
Jacob Nguyen
451eebae7e feat: throw error on plugin usage for event listeners 2022-06-27 13:46:04 -05:00
Jacob Nguyen
9271f103ea feat: throw error on plugin usage for event listeners 2022-06-27 13:20:00 -05:00
Jacob Nguyen
a6d309ab5a refactor: add asyncResolveArray.ts to resolve Awaitables easier 2022-06-27 12:35:07 -05:00
Jacob Nguyen
1b447e3c4f feat: Add more plugin definitions 2022-06-26 23:04:36 -05:00
Jacob Nguyen
c3b16b43ce feat: Add more typings for event modules 2022-06-26 12:51:21 -05:00
Jacob Nguyen
ce06e8158a feat: add enum for event modules 2022-06-24 21:33:18 -05:00
Jacob Nguyen
29b0064329 feat: Separating events from command modules, leads separation of responsibility 2022-06-24 21:33:04 -05:00
Jacob Nguyen
27be769228 feat: move new things to top level import 2022-06-24 15:53:49 -05:00
Jacob Nguyen
106d3d61f9 build: bump to 1.0.4-beta 2022-06-24 11:42:20 -05:00
Jacob Nguyen
5607e6d711 build(package.json): bump version 2022-06-24 10:48:55 -05:00
Jacob Nguyen
dea8fc05b1 fix: Crash on interactionCreate event 2022-06-24 10:45:52 -05:00
Jacob Nguyen
8e037988d5 feat: Add basic event handling, no plugins checked yet! 2022-06-24 00:21:35 -05:00
Jacob Nguyen
c56c3c9a8f fix: complying to djs v14 build 2022-06-23 23:58:08 -05:00
Jacob Nguyen
70ef4a0f8f feat: more progress on event handling 2022-06-20 14:20:41 -05:00
Jacob Nguyen
7456933843 feat: switch to match expression, add docs 2022-06-20 10:43:23 -05:00
Jacob Nguyen
df2e178394 refactor: Adding some type aliases 2022-06-19 18:06:44 -05:00
Jacob Nguyen
62c8fb0e1c feat: Changing types of wrapper for external and sern emitter 2022-06-18 02:16:17 -05:00
Jacob Nguyen
878f67391b refactor: Cleaning up and adding docs 2022-06-18 02:15:37 -05:00
Jacob Nguyen
f020af7062 feat: Cleaning up types 2022-06-17 22:25:19 -05:00
Jacob Nguyen
c56dcc62f3 feat: more event handling progress 2022-06-17 14:54:18 -05:00
Jacob Nguyen
d42ab26417 fix: re add import after rebase 2022-06-16 20:22:01 -05:00
Jacob Nguyen
24cd260531 feat: more support for event loading!! 2022-06-16 20:11:58 -05:00
xxDeveloper
440c9ffd9b style: Improved readyEvent.ts comments 2022-06-16 21:56:28 +03:00
xxDeveloper
9c88951646 style: Improved plugin.ts comments 2022-06-16 21:55:44 +03:00
xxDeveloper
6a983a5eae style: Improved plugin.ts comments 2022-06-15 23:43:22 +03:00
Jacob Nguyen
c17af196fb feat: rolling out more event loading support 2022-06-15 13:02:33 -05:00
Jacob Nguyen
60e7cc2369 feat: revamp module types to support event plugins 2022-06-14 14:47:07 -05:00
Jacob Nguyen
3ccfe6633b refactor: type changes to adjust to event commands 2022-06-14 14:29:25 -05:00
Jacob Nguyen
812974ebb6 feat: add events.ts for more customizable event handling 2022-06-14 14:16:55 -05:00
Jacob Nguyen
cccfecc325 feat: add generic to readFile.ts buildData, adding different event loading strategies 2022-06-14 09:39:29 -05:00
Jacob Nguyen
0fc0782e55 feat: change typings of sern emitter 2022-06-13 01:18:23 -05:00
Jacob Nguyen
120c527b34 feat: loading strategy slightly changed, does not throw error on finding no module in file 2022-06-13 01:17:47 -05:00
Jacob Nguyen
4f7f3b6212 feat: remove docs folder 2022-06-12 13:51:12 -05:00
Jacob Nguyen
8070382229 Merge branch 'api-update' of https://github.com/sern-handler/handler into api-update 2022-06-12 13:49:56 -05:00
Jacob Nguyen
513fffed4b feat: remove edit context method for future 2022-06-12 13:49:32 -05:00
xxDeveloper
232705ca37 fix: Fixed typo at SECURITY.md file 2022-06-11 19:47:58 +03:00
xxDeveloper
7242d8fed1 chore(deps): Removed typedoc
Removed typedoc dependency due we won't use it anymore
2022-06-11 19:44:08 +03:00
Jacob Nguyen
0cc701448b build: update discord.js to latest 2022-06-09 14:34:16 -05:00
Jacob Nguyen
06054db655 fix: intellij warnings 2022-06-08 13:46:31 -05:00
Jacob Nguyen
6be1eea20a feat: Add .edit method and let .reply accept string 2022-06-08 13:46:13 -05:00
xxDeveloper
04517e113f chore: Improved package.json 2022-06-08 12:17:32 -05:00
Jacob Nguyen
028837e1c9 docs: Highly encourage to use cli 2022-06-08 12:17:32 -05:00
Jacob Nguyen
0784c077f5 Update README.md 2022-06-08 12:17:32 -05:00
Jacob Nguyen
231ae24065 feat: Make name and description defined when making plugins! 2022-06-08 11:31:16 -05:00
Jacob Nguyen
ef64d9e99c fix: forget to add SernEvent to Wrapper declaration 2022-06-06 01:01:55 -05:00
Jacob Nguyen
72ceede26a feat: remove jest.config.ts 2022-06-06 00:57:43 -05:00
Jacob Nguyen
5fbd3a6d68 feat: add sern events natively register in sern.init 2022-06-06 00:51:09 -05:00
Jacob Nguyen
df2a92cac2 feat: make description optional, default is '...' 2022-06-05 13:58:09 -05:00
Jacob Nguyen
d4ac129b05 feat: narrow typings for args Text and SlashCommand x3 2022-06-05 13:25:56 -05:00
Jacob Nguyen
04dfa82371 fix: Remove type reduction to never in onReady.ts 2022-06-05 13:24:06 -05:00
Jacob Nguyen
ef357e90af feat: make TextCommand and SlashCommand return more specific arg type 2022-06-05 12:42:17 -05:00
Jacob Nguyen
048072bdd7 feat: make TextCommand and SlashCommand return more specific arg type 2022-06-05 11:27:42 -05:00
Jacob Nguyen
18d07d6e2d feat: more clear error for access message or interaction 2022-06-05 11:17:59 -05:00
Jacob Nguyen
6429085650 feat: make command modules return Awaitable void | unknown 2022-06-05 11:11:46 -05:00
Jacob Nguyen
fd85697636 feat: remove unit testing 2022-06-05 11:07:15 -05:00
68 changed files with 8930 additions and 9756 deletions

View File

@@ -3,11 +3,9 @@
"extends": ["plugin:@typescript-eslint/recommended"],
"parserOptions": { "ecmaVersion": "latest", "sourceType": "script" },
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"semi": ["error", "always"],
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-non-null-assertion": "off",
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals" : true }],
"semi": ["error", "always"],
"@typescript-eslint/no-empty-interface": 0
}
}
}

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @jacoobes
src/* @jacoobes

View File

@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] Write a descriptive title."
labels: bug
assignees: EvolutionX-10, jacoobes, Murtatrxx
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Versioning**
NodeJS version:
DiscordJS version:
SernHandler version:
Channel: (e.g. beta)
**Additional context**
Add any other context about the problem here.

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Sern Community Support
url: https://discord.gg/9w8jzsR48U
about: Please ask and answer questions here.

View File

@@ -1,43 +0,0 @@
---
name: Feature request
about: Ask for things that are not in sern
title: "[Feature] Request a feature"
labels: feature
assignees: EvolutionX-10, jacoobes, Murtatrxx
---
Request a new feature!
---
### Is your proposal related to a problem?
<!--
Provide a clear and concise description of what the problem is.
For example, "I'm always frustrated when..."
-->
(Write your answer here.)
### Describe the solution you'd like
<!--
Provide a clear and concise description of what you want to happen.
-->
(Describe your proposed solution here.)
### Describe alternatives you've considered
<!--
Let us know about other solutions you've tried or researched.
-->
(Write your answer here.)
### Additional context
<!--
Is there anything else you can add about the proposal?
You might want to link to related issues here, if you haven't already.
-->
(Write your answer here.)

View File

@@ -1,26 +0,0 @@
# Description
Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
# Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

View File

@@ -24,14 +24,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v1

View File

@@ -1,44 +0,0 @@
name: Continuous Integration
on:
# Trigger the workflow on push or pull request or custom
push:
branches:
main
pull_request_target:
branches:
main
workflow_dispatch:
jobs:
Prettier:
name: Run Prettier
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
- name: Set up Node.js
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
with:
node-version: 17
# Prettier must be in `package.json`
- name: Install Node.js dependencies
run: npm i -g prettier && npm i
- name: Run Prettier
run: prettier --write .
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
with:
commit-message: "style: pretty please"
branch: prettier
delete-branch: true
branch-suffix: short-commit-hash
title: "style: pretty please"
body: "pretty pretty prettier"
reviewers: EvolutionX-10

View File

@@ -1,20 +0,0 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2

View File

@@ -8,8 +8,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm ci
@@ -19,8 +19,8 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
registry-url: https://registry.npmjs.org/

View File

@@ -1,14 +0,0 @@
name: release-please
on:
workflow_dispatch:
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
with:
release-type: node
package-name: release-please-action
bump-patch-for-minor-pre-major: true

3
.gitignore vendored
View File

@@ -82,8 +82,5 @@ dist
# VisualStudio Config file
.vs
# VSCode settings and cache
.vscode
# IntelliJ IDEA Config file
.idea/

View File

@@ -1,4 +1,5 @@
src/
tsconfig.json
docs/
.gitignore
@@ -8,6 +9,7 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
@@ -104,10 +106,4 @@ CODE_OF_CONDUCT.md
babel.config.js
tsup.config.js
tsconfig-base.json
tsconfig.cjs.json
tsconfig.esm.json
tests/

View File

@@ -1,3 +1,2 @@
.github/
*.md
dist/
*.md

View File

@@ -1,8 +1,8 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"arrowParens": "avoid"
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"arrowParens": "avoid"
}

View File

@@ -1,152 +0,0 @@
# Changelog
## [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)
### ⚠ BREAKING CHANGES
* (2.0 global services) ([#156](https://github.com/sern-handler/handler/issues/156))
### Features
* (2.0 global services) ([#156](https://github.com/sern-handler/handler/issues/156)) ([1455622](https://github.com/sern-handler/handler/commit/14556223fd6f79b797fb2aee03e795d4f4e94a8b))
## [1.2.1](https://github.com/sern-handler/handler/compare/v1.2.0...v1.2.1) (2022-10-03)
### Bug Fixes
* **autocomplete:** now support multiple autocomplete options ([#147](https://github.com/sern-handler/handler/issues/147)) ([cbad738](https://github.com/sern-handler/handler/commit/cbad7380e1993b96c643f365726457f63e4fbd5d))
## [1.2.0](https://github.com/sern-handler/handler/compare/v1.1.0...v1.2.0) (2022-09-28)
### Features
* allow constructable modules ([#133](https://github.com/sern-handler/handler/issues/133)) ([03936eb](https://github.com/sern-handler/handler/commit/03936eb2ea1d1af7cada04d77bb8345d63a5e20f))
* classmodules@arcs ([#143](https://github.com/sern-handler/handler/issues/143)) ([5028886](https://github.com/sern-handler/handler/commit/50288867a5b171511941a1be3877d721694e9f77))
* update CODEOWNERS ([6b8995d](https://github.com/sern-handler/handler/commit/6b8995d149c857558415a6c151a3f575ec373445))
### Reverts
* feat of allow constructable modules ([#138](https://github.com/sern-handler/handler/issues/138)) ([82bbdda](https://github.com/sern-handler/handler/commit/82bbddac8d656b60b3a1fb2471ea03ee5224f5c3))
## [1.1.0](https://github.com/sern-handler/handler/compare/v1.0.0...v1.1.0) (2022-08-29)
### Features
* add proper error handling ([#115](https://github.com/sern-handler/handler/issues/115)) ([395549c](https://github.com/sern-handler/handler/commit/395549c173cb62a18205e451bf2cb5579ba9a6e0))
### Miscellaneous Chores
* release 1.1.0 ([8a373de](https://github.com/sern-handler/handler/commit/8a373de880ff18df85af812adf9f6f6a4f45028d))
## 1.0.0 (2022-08-15)
### ⚠ BREAKING CHANGES
* improve quality of code, refactorings, QOL intellisense (#64)
### Features
* add .prettierignore and ignore README.md ([7ae5ecf](https://github.com/sern-handler/handler/commit/7ae5ecf1a64700d667e85420ae4b2eaf31781d85))
* Add castings for res ([2697e35](https://github.com/sern-handler/handler/commit/2697e35b2e5b754ea9d0d84db3720fb68b3f43db))
* Add DefinetlyDefined type, more todo statements ([c8c0c84](https://github.com/sern-handler/handler/commit/c8c0c841db2423e29d69bbc1a3ab590bfebb5d5b))
* add discord.js as a peerDependency instead ([b3ed8da](https://github.com/sern-handler/handler/commit/b3ed8da68f55b69a7fe1697cd88c552243cc637f))
* add docs/ to npmignore ([f90342d](https://github.com/sern-handler/handler/commit/f90342d6b140241f7a6a95dea71c05bf309a7a52))
* add externallyUsed.ts and support BothCommands again ([fc81bfc](https://github.com/sern-handler/handler/commit/fc81bfc6d75e4722486766715abe7271ad21cd7f))
* add feature-request.md ([#92](https://github.com/sern-handler/handler/issues/92)) ([0d6e592](https://github.com/sern-handler/handler/commit/0d6e592614f0d4eeaaa9ffe5ba245fe002f5b907))
* add frontmatter ([#95](https://github.com/sern-handler/handler/issues/95)) ([75a6a04](https://github.com/sern-handler/handler/commit/75a6a04db56551049387e38979bb7ef21356f303))
* Add messageComponent handler ([d29298c](https://github.com/sern-handler/handler/commit/d29298c17a1d67146bdddb9cf07a16924c55ed3a))
* add version.txt ([4fea383](https://github.com/sern-handler/handler/commit/4fea383519b9905c17c7679587f69b530c08cec8))
* added conventional commits ([741cf13](https://github.com/sern-handler/handler/commit/741cf13fd56ac49ebca6f73ecc3a2209f00e774d))
* Added SECURITY file & formatted some TypeScript code ([779011a](https://github.com/sern-handler/handler/commit/779011a124ab76bbfb19a2a11889bf9255cbd360))
* adding better typings, refactoring ([99e2a99](https://github.com/sern-handler/handler/commit/99e2a997edaac1ba880e56bf782ecd1fa5e96b4c))
* Adding docs to some data structures, moving to default from export files ([0ae541d](https://github.com/sern-handler/handler/commit/0ae541daba4c5d2aa3e612ab4b78fd6a858717ad))
* adding modal and autocomplete support ([77856ce](https://github.com/sern-handler/handler/commit/77856ce5d0d8d1e2e2f5a971269224a4174bc205))
* adding refactoring for repetitive event plugin processing ([475b073](https://github.com/sern-handler/handler/commit/475b0736d573bb8969b2a0eb9180231aa8618a0e))
* Adding sern event listeners, overriding and typing methods ([115d1a4](https://github.com/sern-handler/handler/commit/115d1a49b52eb45d9b68ba015f8f734b902e9a60))
* Adding TextInput map & starting event plugins for message components ([6ac9720](https://github.com/sern-handler/handler/commit/6ac9720260040afb12d232b002c28db99b18e093))
* Aliases optional ([430315a](https://github.com/sern-handler/handler/commit/430315ad02060121e75604aee40c246c71a7e8d2))
* better looking typings for modules ([53bc080](https://github.com/sern-handler/handler/commit/53bc080a290fd5064993aa0d98497d4b239ac8f3))
* broadening EventPlugin default generic type, reformat with prettier ([88dcdee](https://github.com/sern-handler/handler/commit/88dcdee818e42405234ef502087226a8c042c92f))
* CodeQL ([7012da6](https://github.com/sern-handler/handler/commit/7012da60530c2b0b5d8cc97b417a80cd8031f51f))
* delete partition.ts ([f6d584c](https://github.com/sern-handler/handler/commit/f6d584cf99abdb292985f812e64553a37ab51a01))
* Edited event names for more conciseness, finished basic event emitters ([3f64a8a](https://github.com/sern-handler/handler/commit/3f64a8aa0a47a09f822d54f2b3f03bc42faa10f7))
* finished interactionCreate.ts handling? (need test) ([97907b7](https://github.com/sern-handler/handler/commit/97907b746fc94d6e8b65e2fec1cce4b0c3160491))
* finishing autocomplete!! ([d63423c](https://github.com/sern-handler/handler/commit/d63423cfc458cb9ab07b9900a7c4d2f7ea8d71b9))
* finishing optionData for autocomplete changes, adding class for builder ([b08eebf](https://github.com/sern-handler/handler/commit/b08eebf6850acaee3b56bb1c60aec2a026a5144c))
* Finishing up autocomplete, need to test ([d50b801](https://github.com/sern-handler/handler/commit/d50b8013ee343b2be0ed232938e9f5f91c43b493))
* fix duplicate ([c5bd941](https://github.com/sern-handler/handler/commit/c5bd94131dfb20b2c69b7eeb96f3ad89d6de43f4))
* **handler:** adding higher-order-function wrappers to increase readability and shorten code ([0f0b0fb](https://github.com/sern-handler/handler/commit/0f0b0fb61c80654179e2c6d6f69e50c70114201b))
* **handler:** command plugins work?! ([70bd12d](https://github.com/sern-handler/handler/commit/70bd12dd61182f48445c707a9199421b1dba586e))
* **handler:** progress on event plugins ([2f61399](https://github.com/sern-handler/handler/commit/2f61399b5e5ad53ee3165e19cb74dd279b827b99))
* **higherorders.ts:** a new function that acts as a command options builder ([651009c](https://github.com/sern-handler/handler/commit/651009c9ed5e8e04cf44fa4438f94a9e119aa8f8))
* improve quality of code, refactorings, QOL intellisense ([#64](https://github.com/sern-handler/handler/issues/64)) ([e71b63d](https://github.com/sern-handler/handler/commit/e71b63d261d86b17ddc05fbee999f63623d8c6d1))
* Improved TypeScript experince ([dad3042](https://github.com/sern-handler/handler/commit/dad3042a644b0e47d01319f48eefe01632678cc3))
* interactionCreate.ts refactoring ([c4e8e51](https://github.com/sern-handler/handler/commit/c4e8e517b3f4bb6baca902251a0afa22b2548450))
* Making name required in auto cmp interactions ([ac8a2f4](https://github.com/sern-handler/handler/commit/ac8a2f4c86a1c426d32e388a5439a8696db52279))
* move name and description out of OptionsData[] ([93942bd](https://github.com/sern-handler/handler/commit/93942bd0e7d0ac688d20159cab2c70c3285085f4))
* Optional plugins to reduce bloat ([2b81d53](https://github.com/sern-handler/handler/commit/2b81d53503209a738b70d238512c82542d3d88e8))
* **prefix:** make defaultPrefix optional ([f6b88dc](https://github.com/sern-handler/handler/commit/f6b88dcdc80c407e14f4d6ae73eb27e75d195e18))
* **readme.md:** added basic command examples ([63b2d3a](https://github.com/sern-handler/handler/commit/63b2d3a5723ac6e1f0baa0de8b65640cecd7d634))
* remove comments about prev commit ([a220949](https://github.com/sern-handler/handler/commit/a2209494bdd05ca89176aff82f7d3afce0b8de46))
* remove copyright bloat ([48737ef](https://github.com/sern-handler/handler/commit/48737efea3c0fce572238701e72335293333379f))
* remove externallyUsed.ts ([3dec347](https://github.com/sern-handler/handler/commit/3dec347ef0957845601f0eb2acb3f1815d1e9ca1))
* Revamp Docs ([#47](https://github.com/sern-handler/handler/issues/47)) ([163e48f](https://github.com/sern-handler/handler/commit/163e48f3eb38d37500cefc8d0c722c083a3070c7))
* **sern.ts:** adding logging instead of large complaining messages from bot ([00a5fa4](https://github.com/sern-handler/handler/commit/00a5fa43ad9e0b4c7d5ef1f2772a4cb186768837))
* **sern.ts:** beginning to add new basic logger system ([ef9d53e](https://github.com/sern-handler/handler/commit/ef9d53e6b1a9009eab5ce9ff9f8b5542d1d7cf7f))
* **sern.ts:** changed how module is passed around, now has name property at runtime ([40fb723](https://github.com/sern-handler/handler/commit/40fb7231436331c97fa791eab3b8b51636e826f1))
* **sern.ts:** changing default value of args in text based cmd to string[], from string ([1397974](https://github.com/sern-handler/handler/commit/1397974fb6e6d8c1b1e82db8272ef0a57916022c))
* **sern.ts:** changing default value of args in text based cmd to string[], from string ([e0541f7](https://github.com/sern-handler/handler/commit/e0541f777bc3dcb1ec0c0cccf219b9fa66199a2b))
* **sern.ts:** changing text-based command parser fn value to string[] ([b11f999](https://github.com/sern-handler/handler/commit/b11f9996749977a16e516523af7a8e2a9e6521ae))
* **sern.ts:** renaming Module.delegate to Module.execute ([8702876](https://github.com/sern-handler/handler/commit/870287674aa7eccbe1fc890d1cf2cd808982b801))
* should be able to register other nodejs event emitters ([b8cda35](https://github.com/sern-handler/handler/commit/b8cda351e1f549422692c633255ac1d6c7d78a9b))
* shrink package size, improve dev deps, esm and cjs support ([#98](https://github.com/sern-handler/handler/issues/98)) ([74378f0](https://github.com/sern-handler/handler/commit/74378f0f12cf5d16b90ddbc01fb42505e0235c39))
* update example ([0da1b5a](https://github.com/sern-handler/handler/commit/0da1b5a4dc6823807880ade03728b466fe895190))
* Updated Readme design ([369586f](https://github.com/sern-handler/handler/commit/369586f378f807ba9906167b5ada197c2c95e411))
### Bug Fixes
* accidentally imported wildcard from wrong place & namespace ([8782cad](https://github.com/sern-handler/handler/commit/8782cad9cddbb24c03c2bfff96d3377aceb5f542))
* autocomplete in nested form ([#97](https://github.com/sern-handler/handler/issues/97)) ([70d7bdb](https://github.com/sern-handler/handler/commit/70d7bdb8c53a1990addc5c9fd54427f194833b4e))
* Change discord server link ([#62](https://github.com/sern-handler/handler/issues/62)) ([e677ce0](https://github.com/sern-handler/handler/commit/e677ce083966dedc945d236034e2ce4a7a586e05))
* **codeql-analysis.yml:** Fixed autobuild issue on some TS files & deleted more unused comments ([e51c7ff](https://github.com/sern-handler/handler/commit/e51c7ffed038f0519a37f4339406c28546d92c83))
* crash on collectors ([#89](https://github.com/sern-handler/handler/issues/89)) ([a0587f5](https://github.com/sern-handler/handler/commit/a0587f59d43d62642c033e0bb843902f9e6dc0c4))
* crash on collectors pt ([7da7bff](https://github.com/sern-handler/handler/commit/7da7bff700f8e46e72412ca5d379a6edbc14e10a))
* didn't run prettier, now i am ([6c144de](https://github.com/sern-handler/handler/commit/6c144defcacd7732e15292f6c3e5eaea7944bc32))
* Fix return type of sernModule ([cf85a5d](https://github.com/sern-handler/handler/commit/cf85a5db6413e2b8b42143f70964f7a19789e1ff))
* Fixed renovate warning ([#77](https://github.com/sern-handler/handler/issues/77)) ([76c4333](https://github.com/sern-handler/handler/commit/76c4333a817006100f0b99d73bb455e82797d3d9))
* Fixed typo in Security.md ([c35def9](https://github.com/sern-handler/handler/commit/c35def99c93e77a0c932a1b8f1da06cd45fde294))
* **handler.ts:** added the changes of Module<T>.execute to type delegate (now type execute) ([f062a33](https://github.com/sern-handler/handler/commit/f062a338687be4b3ac64c048a63bdcb895282d2d))
* linting issue in markup.ts ([dac665d](https://github.com/sern-handler/handler/commit/dac665d6281a29ec79663adb26a3e5c5243e6ae0))
* Non-exhaustiveness led to commands not registering readyEvent.ts ([b266508](https://github.com/sern-handler/handler/commit/b26650818e2c193c326356359b38412117ea6186))
* prettier changes again ([d5bb992](https://github.com/sern-handler/handler/commit/d5bb9922dfdb14e4f7e95ad5acd470765b7a90c2))
* prettier wants lf line ending ([571a804](https://github.com/sern-handler/handler/commit/571a8044b027afee91466219a841817dd55ef455))
* **sern.ts:** checking ctx instanceof Message always returned false ([7166947](https://github.com/sern-handler/handler/commit/7166947d28f3be1a6e1c44385eb7946731784f58))
* Standard for of does not resolve promises. Switched to for await ([66b9f51](https://github.com/sern-handler/handler/commit/66b9f51fa73cf07a589671d13ca4c65a9c8193a1))
* **utilexports.ts:** forgot to remove the deleted feat of option builder ([1cff46c](https://github.com/sern-handler/handler/commit/1cff46c0ab5959d8e0f0fe89f1e6cd4c6cebff19))
### Reverts
* Move enums to enums.ts ([40a10bf](https://github.com/sern-handler/handler/commit/40a10bf32b9a1c6afbf85bdaeb2a7918c15ac7a8))
* Re-add plugins overload ([b9b5919](https://github.com/sern-handler/handler/commit/b9b59197df7d9bbeac3df68ebe6f7cea1ce50677))
* remove version.txt ([ca9ac52](https://github.com/sern-handler/handler/commit/ca9ac52fae32108b4cb90b201204d5c358c5ef7b))

View File

@@ -1,5 +1,5 @@
# Code of Conduct
All participants of sern are expected to abide by our Code of Conduct, both online and during in-person events that are hosted and/or associated with sern.
All participants of SernHandler are expected to abide by our Code of Conduct, both online and during in-person events that are hosted and/or associated with SernHandler.
# The Pledge
In the interest of fostering an open and welcoming environment, we pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
@@ -26,7 +26,7 @@ Examples of behaviour that contributes to creating a positive environment includ
• Other conduct which you know could reasonably be considered inappropriate in a professional setting. <br/>
• Enforcement. <br/>
Violations of the Code of Conduct may be reported by reaching us on our [discord server](https://discord.com/). All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
Violations of the Code of Conduct may be reported by sending a message to [discord server](https://discord.com/). All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
We hold the right and responsibility to remove comments or other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any members for other behaviours that they deem inappropriate, threatening, offensive, or harmful.

View File

@@ -1,21 +1,13 @@
<div align="center">
<img src="https://raw.githubusercontent.com/sern-handler/.github/main/banner.png" width="900px" />
</div>
# SernHandler
<h1 align="center">Handlers. Redefined.</h1>
<h4 align="center">A customizable, batteries-included, powerful discord.js framework to streamline bot development.</h4>
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/v/@sern/handler?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/dt/@sern/handler?maxAge=3600" alt="NPM downloads" /></a>
[![License: MIT](https://img.shields.io/badge/License-MIT-blavk.svg)](https://opensource.org/licenses/MIT)
<div align="center" styles="margin-top: 10px">
<img src="https://img.shields.io/badge/open-source-brightgreen" />
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/v/@sern/handler?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/@sern/handler"><img src="https://img.shields.io/npm/dt/@sern/handler?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen" alt="License MIT" /></a>
<a href="https://sern.dev"><img alt="docs.rs" src="https://img.shields.io/docsrs/docs" /></a>
<img alt="Lines of code" src="https://img.shields.io/badge/total%20lines-2k-blue" />
</div>
A customizable, batteries-included, powerful discord.js framework to automate and streamline your bot development.
## 📜 Installation
## Installation
```sh
npm install @sern/handler
@@ -29,26 +21,13 @@ yarn add @sern/handler
pnpm add @sern/handler
```
## 👀 Quick Look
* Support for discord.js v14 and all interactions
* Hybrid commands
* Lightweight and customizable
* ESM, CommonJS and TypeScript support
* A powerful CLI and awesome community-made plugins
## 👶 Basic Usage
## Basic Usage
#### ` index.js (CommonJS)`
```js
// Import the discord.js Client and GatewayIntentBits
const { Client, GatewayIntentBits } = require('discord.js');
// Import Sern namespace
const { Sern } = require('@sern/handler');
// Our configuration file
const { Sern } = require('sern-handler');
const { defaultPrefix, token } = require('./config.json');
const client = new Client({
@@ -58,20 +37,11 @@ const client = new Client({
GatewayIntentBits.GuildMessages
]
});
export const useContainer = Sern.makeDependencies({
build: root => root
.add({ '@sern/client': single(client) })
.add({ '@sern/logger': single(new DefaultLogging()) })
});
//View docs for all options
Sern.init({
defaultPrefix: '!', // removing defaultPrefix will shut down text commands
commands: 'src/commands',
// events: 'src/events' (optional),
containerConfig : {
get: useContainer
}
client,
defaultPrefix,
commands : 'src/commands',
});
client.login(token);
@@ -80,35 +50,34 @@ client.login(token);
#### ` ping.js (CommonJS)`
```js
const { CommandType, commandModule } = require('@sern/handler');
const { Sern, CommandType } = require('@sern/handler');
exports.default = commandModule({
name: 'ping',
description: 'A ping pong command',
type: CommandType.Slash,
execute(ctx) {
ctx.reply('pong!');
}
});
exports.default = {
description: 'A ping pong command',
type: CommandType.Slash,
execute(ctx) {
ctx.reply('pong!');
}
};
```
See our [templates](https://github.com/sern-handler/templates) for TypeScript examples and more.
See our [templates](https://github.com/sern-handler/templates) for TypeScript examples and more
## 💻 CLI
## CLI
It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it.
## 🔗 Links
## Links
- [Official Documentation and Guide](https://sern.dev)
- [Support Server](https://sern.dev/discord)
- [Official Documentation](https://sern-handler.js.org)
- [Support Server](https://discord.com/invite/mmyCTnYtbF)
## 👋 Contribute
## Contribute
- Read our contribution [guidelines](./.github/CONTRIBUTING.md) carefully
- Read our contribution [guidelines](https://github.com/sern-handler/handler) carefully
- Pull up on [issues](https://github.com/sern-handler/handler/issues) and report bugs
- All kinds of contributions are welcomed.
## 🚈 Roadmap
## Roadmap
You can check our [roadmap](https://github.com/sern-handler/roadmap) to see what's going to be added or patched in the future.

12441
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +1,50 @@
{
"name": "@sern/handler",
"packageManager": "pnpm@7.18.2",
"version": "2.1.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",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs"
}
},
"scripts": {
"watch": "tsup --watch --dts",
"clean-modules": "rimraf node_modules/ && npm install",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
"build": "tsup && node scripts/mkjson.mjs dist/cjs dist/esm && tsup --dts-only --outDir dist",
"publish": "npm run build && npm publish",
"pretty": "prettier --write ."
},
"keywords": [
"sern-handler",
"sern",
"handler",
"sern handler",
"wrapper",
"discord.js",
"framework"
],
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"iti": "^0.5.0",
"rxjs": "^7.5.6",
"ts-pattern": "^4.0.2",
"ts-results-es": "^3.5.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.47.1",
"@typescript-eslint/parser": "5.47.1",
"eslint": "8.30.0",
"prettier": "2.8.1",
"tsup": "^6.1.3",
"typescript": "4.9.4",
"discord.js": ">= ^14.7.x"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/handler.git"
},
"homepage": "https://sern.dev"
"name": "@sern/handler",
"version": "1.1.0-beta",
"description": "A customizable, batteries-included, powerful discord.js framework to automate and streamline bot development.",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"watch": "tsc -w",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
"release": "standard-version && git push --follow-tags",
"commit": "cz"
},
"keywords": [
"sern-handler",
"sern",
"handler",
"sern handler",
"wrapper",
"discord.js",
"framework"
],
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"discord.js": "^14.0.0-dev.1647259751.2297c2b",
"rxjs": "^7.5.5",
"ts-pattern": "^4.0.2",
"ts-results": "^3.3.0"
},
"devDependencies": {
"eslint": "^8.8.0",
"@typescript-eslint/parser": "^5.10.2",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"cz-conventional-changelog": "3.0.1",
"prettier": "2.6.2",
"standard-version": "^9.3.2",
"typescript": "4.5.5"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/handler.git"
},
"homepage": "https://sern-handler.js.org",
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}

2443
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
{
"major": {
"dependencyDashboardApproval": true,
"reviewers": ["EvolutionX-10", "jacoobes", "Murtatrxx"]
},
"minor": {
"reviewers": ["jacoobes", "Murtatrxx"]
},
"schedule": ["every weekend"],
"lockFileMaintenance": {
"enabled": true,
"automerge": false
}
}

View File

@@ -1,13 +0,0 @@
import { writeFile } from 'fs/promises';
import { join } from 'path';
// A quick script to regenerate package.jsons for each cjs and esm after tsup cleans distributions
const locations = process.argv;
locations.shift();
locations.shift();
for (const loc of locations) {
if (loc.endsWith('cjs')) {
await writeFile(join(loc, 'package.json'), JSON.stringify({ type: 'commonjs' }));
} else {
await writeFile(join(loc, 'package.json'), JSON.stringify({ type: 'module' }));
}
}

View File

@@ -1,51 +0,0 @@
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;
/**
* Utility function to crash
* @param error
*/
crash(error: Error): never;
/**
* A function that is called on every crash. Updates keepAlive
* @param error
*/
updateAlive(error: Error): void;
}
export class DefaultErrorHandling implements ErrorHandling {
keepAlive = 5;
crash(error: Error): never {
throw error;
}
updateAlive(e: Error) {
this.keepAlive--;
}
}
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' });
});
crashHandler.crash(err);
}
//formatted payload
logging?.error({ message: util.format(pload) });
crashHandler.updateAlive(err);
return caught;
};
}

View File

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

View File

@@ -1,27 +0,0 @@
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;
}
export class DefaultLogging implements Logging {
private date = () => new Date();
debug(payload: LogPayload): void {
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
}
error(payload: LogPayload): void {
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
}
info(payload: LogPayload): void {
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
}
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}

View File

@@ -1,21 +0,0 @@
import type { CommandModuleDefs } from '../../types/module';
import type { CommandType } from '../structures/enums';
import type { ModuleStore } from '../structures/moduleStore';
export interface ModuleManager {
get<T extends CommandType>(
strat: (ms: ModuleStore) => 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) {
return strat(this.moduleStore);
}
set(strat: (ms: ModuleStore) => void): void {
strat(this.moduleStore);
}
}

View File

@@ -1 +0,0 @@
export { useContainer } from './provider';

View File

@@ -1,90 +0,0 @@
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 SernEmitter from '../sernEmitter';
import { _const, ok } from '../utilities/functions';
import { DefaultErrorHandling, DefaultModuleManager, Logging } from '../contracts';
import { ModuleStore } from '../structures/moduleStore';
import { Ok, Result } from 'ts-results-es';
import { DefaultLogging } from '../contracts';
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);
}
};
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());
}
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?
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.
* Exposes some methods from iti
*/
export function useContainerRaw() {
return containerSubject.getValue();
}

View File

@@ -1,209 +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

@@ -1,31 +0,0 @@
import type Wrapper from '../structures/wrapper';
import { Subject, type Observable } from 'rxjs';
import type { EventEmitter } from 'events';
import type SernEmitter from '../sernEmitter';
import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
export abstract class EventsHandler<T> {
protected payloadSubject = new Subject<T>();
protected abstract discordEvent: Observable<unknown>;
protected client: EventEmitter;
protected emitter: SernEmitter;
protected crashHandler: ErrorHandling;
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;
this.modules = modules as ModuleManager;
this.client = client as EventEmitter;
this.emitter = emitter as SernEmitter;
this.crashHandler = crash as ErrorHandling;
}
protected abstract init(): void;
protected abstract setState(state: T): void;
}

View File

@@ -0,0 +1,229 @@
import type {
CommandInteraction,
Interaction,
MessageComponentInteraction,
ModalSubmitInteraction,
SelectMenuInteraction,
} from 'discord.js';
import { concatMap, fromEvent, map, Observable, of, throwError } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import * as Files from '../utilities/readFile';
import { match } from 'ts-pattern';
import { SernError } from '../structures/errors';
import Context from '../structures/context';
import { controller } from '../sern';
import type { Module } from '../structures/module';
import {
isApplicationCommand,
isAutocomplete,
isButton,
isChatInputCommand,
isMessageComponent,
isMessageCtxMenuCmd,
isModalSubmit,
isSelectMenu,
isUserContextMenuCmd,
} from '../utilities/predicates';
import { filterCorrectModule } from './observableHandling';
import { CommandType } from '../structures/enums';
import type { AutocompleteInteraction } from 'discord.js';
import { asyncResolveArray } from '../utilities/asyncResolveArray';
function applicationCommandHandler(mod: Module | undefined, interaction: CommandInteraction) {
const mod$ = <T extends CommandType>(cmdTy: T) => of(mod).pipe(filterCorrectModule(cmdTy));
return (
match(interaction)
.when(isChatInputCommand, i => {
const ctx = Context.wrap(i);
return mod$(CommandType.Slash).pipe(
concatMap(m => {
return of(
m.onEvent.map(e => e.execute([ctx, ['slash', i.options]], controller)),
).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx, ['slash', i.options]);
},
})),
);
}),
);
})
//Todo: refactor so that we dont have to have two separate branches. They're near identical!!
//Only thing that differs is type of interaction
.when(isMessageCtxMenuCmd, ctx => {
return mod$(CommandType.MenuMsg).pipe(
concatMap(m => {
return of(m.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx);
},
})),
);
}),
);
})
.when(isUserContextMenuCmd, ctx => {
return mod$(CommandType.MenuUser).pipe(
concatMap(m => {
return of(m.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx);
},
})),
);
}),
);
})
.run()
);
}
function messageComponentInteractionHandler(
mod: Module | undefined,
interaction: MessageComponentInteraction,
) {
const mod$ = <T extends CommandType>(ty: T) => of(mod).pipe(filterCorrectModule(ty));
//Todo: refactor so that we dont have to have two separate branches. They're near identical!!
//Only thing that differs is type of interaction
return match(interaction)
.when(isButton, ctx => {
return mod$(CommandType.Button).pipe(
concatMap(m => {
return of(m.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx);
},
})),
);
}),
);
})
.when(isSelectMenu, (ctx: SelectMenuInteraction) => {
return mod$(CommandType.MenuSelect).pipe(
concatMap(m => {
return of(m.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return m.execute(ctx);
},
})),
);
}),
);
})
.otherwise(() => throwError(() => SernError.NotSupportedInteraction));
}
function modalHandler(modul: Module | undefined, ctx: ModalSubmitInteraction) {
return of(modul).pipe(
filterCorrectModule(CommandType.Modal),
concatMap(mod => {
return of(mod.onEvent.map(e => e.execute([ctx], controller))).pipe(
map(res => ({
mod,
res,
execute() {
return mod.execute(ctx);
},
})),
);
}),
);
}
function autoCmpHandler(mod: Module | undefined, interaction: AutocompleteInteraction) {
return of(mod).pipe(
filterCorrectModule(CommandType.Slash),
concatMap(mod => {
const choice = interaction.options.getFocused(true);
const selectedOption = mod.options?.find(o => o.autocomplete && o.name === choice.name);
if (selectedOption !== undefined && selectedOption.autocomplete) {
return of(
selectedOption.command.onEvent.map(e => e.execute(interaction, controller)),
).pipe(
map(res => ({
mod,
res,
execute() {
return selectedOption.command.execute(interaction);
},
})),
);
}
return throwError(
() =>
SernError.NotSupportedInteraction +
` There is probably no autocomplete tag for this option`,
);
}),
);
}
export function onInteractionCreate(wrapper: Wrapper) {
const { client } = wrapper;
const interactionEvent$ = <Observable<Interaction>>fromEvent(client, 'interactionCreate');
interactionEvent$
.pipe(
/*processing plugins*/
concatMap(interaction => {
if (isApplicationCommand(interaction)) {
const modul =
Files.ApplicationCommands[interaction.commandType].get(
interaction.commandName,
) ?? Files.BothCommands.get(interaction.commandName);
return applicationCommandHandler(modul, interaction);
}
if (isMessageComponent(interaction)) {
const modul = Files.MessageCompCommands[interaction.componentType].get(
interaction.customId,
);
return messageComponentInteractionHandler(modul, interaction);
}
if (isModalSubmit(interaction)) {
const modul = Files.ModalSubmitCommands.get(interaction.customId);
return modalHandler(modul, interaction);
}
if (isAutocomplete(interaction)) {
const modul =
Files.ApplicationCommands['1'].get(interaction.commandName) ??
Files.BothCommands.get(interaction.commandName);
return autoCmpHandler(modul, interaction);
}
return throwError(() => SernError.NotSupportedInteraction);
}),
)
.subscribe({
async next({ mod, res: eventPluginRes, execute }) {
const ePlugArr = await asyncResolveArray(eventPluginRes);
if (ePlugArr.every(e => e.ok)) {
await execute();
wrapper.sernEmitter?.emit('module.activate', { type: 'success', module: mod! });
} else {
wrapper.sernEmitter?.emit('module.activate', {
type: 'failure',
module: mod!,
reason: SernError.PluginFailure,
});
}
},
error(err) {
wrapper.sernEmitter?.emit('error', err);
},
});
}

View File

@@ -1,127 +0,0 @@
import type { Interaction } from 'discord.js';
import { catchError, concatMap, from, 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 { 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 type { CommandModule } from '../../types/module';
import { handleError } from '../contracts/errorHandling';
import type { ModuleStore } from '../structures/moduleStore';
import type { MessageComponentInteraction } from 'discord.js';
export default class InteractionHandler extends EventsHandler<{
event: Interaction;
mod: CommandModule;
}> {
protected override discordEvent: Observable<Interaction>;
constructor(wrapper: Wrapper) {
super(wrapper);
this.discordEvent = <Observable<Interaction>>fromEvent(this.client, 'interactionCreate');
this.init();
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)),
catchError(handleError(this.crashHandler, this.logger)),
)
.subscribe();
}
override init() {
const get = (cb: (ms: ModuleStore) => CommandModule | undefined) => {
return this.modules.get(cb);
};
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),
);
this.setState({ event, mod });
} else if (event.isModalSubmit()) {
const mod = get(ms => ms.InteractionHandlers[5].get(event.customId));
this.setState({ event, mod });
} else {
throw Error('This interaction is not supported yet');
}
},
error: reason => {
this.emitter.emit('error', { type: PayloadType.Failure, 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',
});
} else {
//if statement above checks already, safe cast
this.payloadSubject.next(state as { event: Interaction; mod: 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)));
}
}

View File

@@ -0,0 +1,71 @@
import type { Message } from 'discord.js';
import { concatMap, from, fromEvent, map, Observable, of } from 'rxjs';
import { controller } from '../sern';
import Context from '../structures/context';
import type Wrapper from '../structures/wrapper';
import { fmt } from '../utilities/messageHelpers';
import * as Files from '../utilities/readFile';
import { filterCorrectModule, ignoreNonBot } from './observableHandling';
import { CommandType } from '../structures/enums';
import { SernError } from '../structures/errors';
import { asyncResolveArray } from '../utilities/asyncResolveArray';
export const onMessageCreate = (wrapper: Wrapper) => {
const { client, defaultPrefix } = wrapper;
if (defaultPrefix === undefined) return;
const messageEvent$ = <Observable<Message>>fromEvent(client, 'messageCreate');
const processMessage$ = messageEvent$.pipe(
ignoreNonBot(defaultPrefix),
map(message => {
const [prefix, ...rest] = fmt(message, defaultPrefix);
return {
ctx: Context.wrap(message),
args: <['text', string[]]>['text', rest],
mod:
Files.TextCommands.text.get(prefix) ??
Files.BothCommands.get(prefix) ??
Files.TextCommands.aliases.get(prefix),
};
}),
);
const ensureModuleType$ = processMessage$.pipe(
concatMap(payload =>
of(payload.mod).pipe(
filterCorrectModule(CommandType.Text),
map(mod => ({ ...payload, mod })),
),
),
);
const processEventPlugins$ = ensureModuleType$.pipe(
concatMap(({ ctx, args, mod }) => {
const res = asyncResolveArray(
mod.onEvent.map(ePlug => {
return ePlug.execute([ctx, args], controller);
}),
);
return from(res).pipe(map(res => ({ mod, ctx, args, res })));
}),
);
processEventPlugins$.subscribe({
next({ mod, ctx, args, res }) {
if (res.every(pl => pl.ok)) {
Promise.resolve(mod.execute(ctx, args)).then(() => {
wrapper.sernEmitter?.emit('module.activate', { type: 'success', module: mod! });
});
} else {
wrapper.sernEmitter?.emit('module.activate', {
type: 'failure',
module: mod!,
reason: SernError.PluginFailure,
});
}
},
error(e) {
wrapper.sernEmitter?.emit('error', e);
},
});
};

View File

@@ -1,74 +0,0 @@
import { EventsHandler } from './eventsHandler';
import { catchError, concatMap, from, fromEvent, map, Observable, of, switchMap } from 'rxjs';
import type Wrapper from '../structures/wrapper';
import type { Message } from 'discord.js';
import { executeModule, ignoreNonBot, isOneOfCorrectModules } 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 { handleError } from '../contracts/errorHandling';
import type { ModuleStore } from '../structures/moduleStore';
export default class MessageHandler extends EventsHandler<{
ctx: Context;
args: ['text', string[]];
mod: TextCommand;
}> {
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)),
);
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)),
catchError(handleError(this.crashHandler, this.logger)),
)
.subscribe();
}
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);
};
this.discordEvent
.pipe(
ignoreNonBot(this.wrapper.defaultPrefix),
map(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)),
};
}),
concatMap(element =>
of(element.mod).pipe(
isOneOfCorrectModules(CommandType.Text),
map(mod => ({ ...element, mod })),
),
),
)
.subscribe({
next: value => this.setState(value),
error: reason => this.emitter.emit('error', { type: PayloadType.Failure, reason }),
});
}
protected setState(state: { ctx: Context; args: ['text', string[]]; mod: TextCommand }) {
this.payloadSubject.next(state);
}
}

View File

@@ -1,16 +1,28 @@
import type { Message } from 'discord.js';
import { concatMap, from, map, Observable, of, switchMap, tap, throwError, toArray } from 'rxjs';
import { Observable, throwError } from 'rxjs';
import { SernError } from '../structures/errors';
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 { Module, CommandModuleDefs } from '../structures/module';
import { correctModuleType } from '../utilities/predicates';
import type { Result } from 'ts-results';
export function filterCorrectModule<T extends keyof CommandModuleDefs>(cmdType: T) {
return (src: Observable<Module | undefined>) =>
new Observable<CommandModuleDefs[T]>(subscriber => {
return src.subscribe({
next(mod) {
if (mod === undefined) {
return throwError(() => SernError.UndefinedModule);
}
if (correctModuleType(mod, cmdType)) {
subscriber.next(mod!);
} else {
return throwError(() => SernError.MismatchModule);
}
},
error: e => subscriber.error(e),
complete: () => subscriber.complete(),
});
});
}
export function ignoreNonBot(prefix: string) {
return (src: Observable<Message>) =>
@@ -36,7 +48,7 @@ export function ignoreNonBot(prefix: string) {
* If the current value in Result stream is an error, calls callback.
* @param cb
*/
export function errTap<T extends AnyModule>(cb: (err: SernError) => void) {
export function errTap<T extends Module>(cb: (err: SernError) => void) {
return (src: Observable<Result<{ mod: T; absPath: string }, SernError>>) =>
new Observable<{ mod: T; absPath: string }>(subscriber => {
return src.subscribe({
@@ -52,100 +64,3 @@ export function errTap<T extends AnyModule>(cb: (err: SernError) => void) {
});
});
}
//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(),
});
});
};
}
export function executeModule(
wrapper: Wrapper,
payload: {
mod: CommandModule;
execute: () => unknown;
res: Result<void, void>[];
},
) {
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 })),
);
}
export function processPlugins(payload: {
mod: DefinedCommandModule | DefinedEventModule;
absPath: string;
}) {
const cmdPluginRes = processCommandPlugins(payload);
return of({ mod: payload.mod, cmdPluginRes });
}

View File

@@ -0,0 +1,125 @@
import { concat, concatMap, from, fromEvent, map, Observable, of, skip, take } from 'rxjs';
import { basename } from 'path';
import * as Files from '../utilities/readFile';
import type Wrapper from '../structures/wrapper';
import type { Result } from 'ts-results';
import { Err, Ok } from 'ts-results';
import type { Awaitable } from 'discord.js';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { CommandModule } from '../structures/module';
import { match } from 'ts-pattern';
import { SernError } from '../structures/errors';
import type { DefinedCommandModule } from '../../types/handler';
import { CommandType, PluginType } from '../structures/enums';
import { errTap } from './observableHandling';
import { processCommandPlugins } from './userDefinedEventsHandling';
export function onReady(wrapper: Wrapper) {
const { client, commands } = wrapper;
const ready$ = fromEvent(client, 'ready').pipe(take(1), skip(1));
// Using sernModule function already checks if module is not EventModule
const processCommandFiles$ = Files.buildData<CommandModule>(commands).pipe(
errTap(reason => {
wrapper.sernEmitter?.emit('module.register', {
type: 'failure',
module: undefined,
reason,
});
}),
map(({ mod, absPath }) => {
return {
absPath,
mod: <DefinedCommandModule>{
name: mod?.name ?? Files.fmtFileName(basename(absPath)),
description: mod?.description ?? '...',
...mod,
},
};
}),
);
const processPlugins$ = processCommandFiles$.pipe(
concatMap(payload => {
const cmdPluginRes = processCommandPlugins(wrapper, payload);
return of({ mod: payload.mod, cmdPluginRes });
}),
);
(
concat(ready$, processPlugins$) as Observable<{
mod: DefinedCommandModule;
cmdPluginRes: {
execute: Awaitable<Result<void, void>>;
type: PluginType.Command;
name: string;
description: string;
}[];
}>
)
.pipe(
concatMap(pl => {
return from(
//refactor, this allocates too many objects
Promise.all(
pl.cmdPluginRes.map(async e => ({ ...e, execute: await e.execute })),
),
).pipe(map(res => ({ ...pl, cmdPluginsRes: res })));
}),
)
.subscribe(({ mod, cmdPluginsRes }) => {
const loadedPluginsCorrectly = cmdPluginsRes.every(({ execute }) => execute.ok);
if (loadedPluginsCorrectly) {
const res = registerModule(mod);
if (res.err) {
throw Error(SernError.NonValidModuleType);
}
wrapper.sernEmitter?.emit('module.register', { type: 'success', module: mod });
} else {
wrapper.sernEmitter?.emit('module.register', {
type: 'failure',
module: mod,
reason: SernError.PluginFailure,
});
}
});
}
function registerModule(mod: DefinedCommandModule): Result<void, void> {
const name = mod.name;
return match<DefinedCommandModule>(mod)
.with({ type: CommandType.Text }, mod => {
mod.alias?.forEach(a => Files.TextCommands.aliases.set(a, mod));
Files.TextCommands.text.set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Slash }, mod => {
Files.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Both }, mod => {
Files.BothCommands.set(name, mod);
mod.alias?.forEach(a => Files.TextCommands.aliases.set(a, mod));
return Ok.EMPTY;
})
.with({ type: CommandType.MenuUser }, mod => {
Files.ApplicationCommands[ApplicationCommandType.User].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.MenuMsg }, mod => {
Files.ApplicationCommands[ApplicationCommandType.Message].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Button }, mod => {
Files.ApplicationCommands[ComponentType.Button].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.MenuSelect }, mod => {
Files.MessageCompCommands[ComponentType.SelectMenu].set(name, mod);
return Ok.EMPTY;
})
.with({ type: CommandType.Modal }, mod => {
Files.ModalSubmitCommands.set(name, mod);
return Ok.EMPTY;
})
.otherwise(() => Err.EMPTY);
}

View File

@@ -1,133 +0,0 @@
import { EventsHandler } from './eventsHandler';
import type Wrapper from '../structures/wrapper';
import { concatMap, fromEvent, Observable, map, 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 { 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 { ModuleManager } from '../contracts';
import type { ModuleStore } from '../structures/moduleStore';
import { _const, err, nameOrFilename, ok } from '../utilities/functions';
export default class ReadyHandler extends EventsHandler<{
mod: DefinedCommandModule;
absPath: string;
}> {
protected discordEvent!: Observable<{ mod: 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,
}),
),
),
),
);
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,
});
}
});
}
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({
next: value => this.setState(value),
complete: () => this.payloadSubject.unsubscribe(),
});
}
protected setState(state: { absPath: string; mod: DefinedCommandModule }): void {
this.payloadSubject.next(state);
}
}
function registerModule(
manager: ModuleManager,
mod: DefinedCommandModule | DefinedEventModule,
): 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)
.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)),
)
.with({ type: CommandType.Both }, mod => {
mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
return insert(ms => ms.BothCommands.set(name, mod));
})
.with({ type: CommandType.CtxUser }, mod =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod)),
)
.with({ type: CommandType.CtxMsg }, mod =>
insert(ms => ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod)),
)
.with({ type: CommandType.Button }, mod =>
insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod)),
)
.with({ type: CommandType.StringSelect }, 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.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.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,124 +1,93 @@
import { catchError, concatMap, filter, from, iif, map, of, tap, toArray } from 'rxjs';
import { buildData } from '../utilities/readFile';
import { from, fromEvent, map } from 'rxjs';
import * as Files from '../utilities/readFile';
import { buildData, ExternalEventEmitters } from '../utilities/readFile';
import { controller } from '../sern';
import type { DefinedCommandModule, DefinedEventModule, Dependencies } from '../../types/handler';
import { PayloadType, PluginType } from '../structures/enums';
import type { DefinedCommandModule, DefinedEventModule, SpreadParams } from '../../types/handler';
import type { EventModule } from '../structures/module';
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 { EventEmitter } from 'events';
import type SernEmitter from '../sernEmitter';
import { nameOrFilename, reducePlugins } from '../utilities/functions';
import { basename } from 'path';
import { match } from 'ts-pattern';
import {
discordEventDispatcher,
externalEventDispatcher,
sernEmitterDispatcher,
} from './dispatchers';
import type { ErrorHandling, Logging } from '../contracts';
import { SernError } from '../structures/errors';
import { handleError } from '../contracts/errorHandling';
import type { Awaitable } from 'discord.js';
import type { Result } from 'ts-results-es';
import { isDiscordEvent, isSernEvent } from '../utilities/predicates';
import { errTap } from './observableHandling';
/**
* Utility function to process command plugins for all Modules
* @param wrapper
* @param payload
*/
export function processCommandPlugins<
T extends DefinedCommandModule | DefinedEventModule,
>(payload: {
mod: T;
absPath: string;
}): { type: PluginType.Command; execute: Awaitable<Result<void, void>> }[] {
export function processCommandPlugins<T extends DefinedCommandModule>(
wrapper: Wrapper,
payload: { mod: T; absPath: string },
) {
return payload.mod.plugins.map(plug => ({
type: plug.type,
execute: plug.execute(payload as any, controller),
...plug,
name: plug?.name ?? 'Unnamed Plugin',
description: plug?.description ?? '...',
execute: plug.execute(wrapper, payload, controller),
}));
}
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 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),
export function processEvents(
wrapper: Wrapper,
events:
| string
| { mod: EventModule; absPath: string }[]
| (() => { mod: EventModule; absPath: string }[]),
) {
const eventStream$ = eventObservable$(wrapper, events);
const normalize$ = eventStream$.pipe(
map(({ mod, absPath }) => {
return <DefinedEventModule>{
name: mod?.name ?? Files.fmtFileName(basename(absPath)),
description: mod?.description ?? '...',
...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),
),
),
};
}),
);
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) {
if (Array.isArray(event)) {
payload.cmd.execute(...event);
} else {
payload.cmd.execute(event as never);
}
}
}),
catchError(handleError(error, logging)),
),
),
)
.subscribe();
normalize$.subscribe(e => {
const emitter = isSernEvent(e)
? wrapper?.sernEmitter
: isDiscordEvent(e)
? wrapper.client
: ExternalEventEmitters.get(e.emitter);
if (emitter === undefined) {
throw new Error(`Cannot find event emitter as it is undefined`);
}
//Would add sern event emitter for events loaded, attached onto sern emitter, but could lead to unwanted behavior!
fromEvent(emitter, e.name, e.execute as SpreadParams<typeof e.execute>).subscribe();
});
}
function eventObservable$(events: string, emitter: SernEmitter) {
return buildData<EventModule>(events).pipe(
errTap(reason =>
emitter.emit('module.register', {
type: PayloadType.Failure,
module: undefined,
reason,
}),
),
);
function eventObservable$(
{ sernEmitter }: Wrapper,
events:
| string
| { mod: EventModule; absPath: string }[]
| (() => { mod: EventModule; absPath: string }[]),
) {
return match(events)
.when(Array.isArray, (arr: { mod: EventModule; absPath: string }[]) => {
return from(arr);
})
.when(
e => typeof e === 'string',
(eventsDir: string) => {
return buildData<EventModule>(eventsDir).pipe(
errTap(reason =>
sernEmitter?.emit('module.register', {
type: 'failure',
module: undefined,
reason,
}),
),
);
},
)
.when(
e => typeof e === 'function',
(evs: () => { mod: EventModule; absPath: string }[]) => {
return from(evs());
},
)
.run();
}

View File

@@ -7,103 +7,140 @@
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cool-downs, permissions, etc.
* categorizing commands, cooldowns, 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 { AutocompleteInteraction, Awaitable, Client, ClientEvents } from 'discord.js';
import type { Err, Ok, Result } from 'ts-results';
import type { CommandType, DefinitelyDefined, Override, SernEventsMapping } from '../..';
import { EventType, PluginType } from '../..';
import type { BaseModule, CommandModuleDefs, EventModuleDefs } from '../structures/module';
import type { EventEmitter } from 'events';
import type {
DiscordEventCommand,
ExternalEventCommand,
SernEventCommand,
} from '../structures/events';
import type SernEmitter from '../sernEmitter';
import type Wrapper from '../structures/wrapper';
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>>;
}
type BasePlugin = Override<
BaseModule,
{
type: PluginType;
}
>;
export interface ExternalEmitterPlugin extends Plugin {
type: PluginType.Command;
execute: (
payload: { mod: ExternalEventCommand & { name: string }; absPath: string },
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export type CommandPlugin<T extends keyof CommandModuleDefs = keyof CommandModuleDefs> = {
[K in T]: Override<
BasePlugin,
{
type: PluginType.Command;
execute: (
wrapper: Wrapper,
payload: {
mod: DefinitelyDefined<CommandModuleDefs[T], 'name' | 'description'>;
absPath: string;
},
controller: Controller,
) => Awaitable<Result<void, void>>;
}
>;
}[T];
export interface SernEmitterPlugin extends Plugin {
type: PluginType.Command;
execute: (
payload: { mod: SernEventCommand & { name: string }; absPath: string },
controller: Controller,
) => Awaitable<Result<void, void>>;
}
export type DiscordEmitterPlugin = Override<
BasePlugin,
{
type: PluginType.Command;
execute: (
wrapper: Client,
module: DefinitelyDefined<DiscordEventCommand, 'name' | 'description'>,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
>;
export type ExternalEmitterPlugin<T extends EventEmitter = EventEmitter> = Override<
BasePlugin,
{
type: PluginType.Command;
execute: (
wrapper: T,
module: DefinitelyDefined<ExternalEventCommand, 'name' | 'description'>,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
>;
export interface AutocompletePlugin extends Plugin {
type: PluginType.Event;
execute: (
autocmp: AutocompleteInteraction,
controlller: Controller,
) => Awaitable<Result<void, void>>;
}
export type SernEmitterPlugin = Override<
BasePlugin,
{
type: PluginType.Command;
execute: (
wrapper: SernEmitter,
module: DefinitelyDefined<SernEventCommand, 'name' | 'description'>,
controller: 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 type AutocompletePlugin = Override<
BaseModule,
{
type: PluginType.Event;
execute: (
autocmp: AutocompleteInteraction,
controlller: 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 type EventPlugin<T extends keyof CommandModuleDefs = keyof CommandModuleDefs> = {
[K in T]: Override<
BasePlugin,
{
type: PluginType.Event;
execute: (
event: Parameters<CommandModuleDefs[K]['execute']>,
controller: Controller,
) => Awaitable<Result<void, void>>;
}
>;
}[T];
export interface ExternalEventPlugin extends Plugin {
type: PluginType.Event;
execute: (args: unknown[], controller: Controller) => Awaitable<Result<void, void>>;
}
export type SernEventPlugin<T extends keyof SernEventsMapping = keyof SernEventsMapping> = Override<
BasePlugin,
{
name?: T;
type: PluginType.Event;
execute: (
args: SernEventsMapping[T],
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 ExternalEventPlugin = Override<
BasePlugin,
{
type: PluginType.Event;
execute: (args: unknown[], controller: Controller) => Awaitable<Result<void, void>>;
}
>;
export type DiscordEventPlugin<T extends keyof ClientEvents = keyof ClientEvents> = Override<
BasePlugin,
{
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'>;
@@ -134,6 +171,8 @@ export type EventModulePlugin<T extends EventType> =
| EventModuleCommandPluginDefs[T];
export type CommandModulePlugin<T extends CommandType> = EventPlugin<T> | CommandPlugin<T>;
//TODO: I WANT BETTER TYPINGS AHHHHHHHHHHHHHHH
// Maybe add overlaods
/**
* User inputs this type. Sern processes behind the scenes for better usage

View File

@@ -1,8 +1,14 @@
import type Wrapper from './structures/wrapper';
import { onReady } from './events/readyEvent';
import { onMessageCreate } from './events/messageEvent';
import { onInteractionCreate } from './events/interactionCreate';
import { Err, Ok } from 'ts-results';
import { ExternalEventEmitters } from './utilities/readFile';
import type { EventEmitter } from 'events';
import { processEvents } from './events/userDefinedEventsHandling';
import { CommandType, EventType, PluginType } from './structures/enums';
import type { CommandModule, EventModule } from './structures/module';
import { EventType, PluginType } from './structures/enums';
import type {
Plugin,
CommandPlugin,
EventModuleCommandPluginDefs,
EventModuleEventPluginDefs,
@@ -10,111 +16,86 @@ import type {
InputCommandModule,
InputEventModule,
} from './plugins/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 { Logging } from './contracts';
import { err, ok, partition } from './utilities/functions';
import { SernError } from './structures/errors';
/**
*
* @param wrapper Options to pass into sern.
* Function to start the handler up
* @example
* ```ts title="src/index.ts"
* Sern.init({
* defaultPrefix: '!',
* commands: 'dist/commands',
* events: 'dist/events',
* containerConfig : {
* get: useContainer
* }
* })
* ```
* @param wrapper options to pass into sern.
* Function to start the handler up.
*/
export function init(wrapper: Wrapper) {
const logger = wrapper.containerConfig.get('@sern/logger')[0] as Logging | undefined;
const startTime = performance.now();
const { events } = wrapper;
if (events !== undefined) {
processEvents(wrapper);
processEvents(wrapper, events);
}
new ReadyHandler(wrapper);
new MessageHandler(wrapper);
new InteractionHandler(wrapper);
const endTime = performance.now();
logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
onReady(wrapper);
onMessageCreate(wrapper);
onInteractionCreate(wrapper);
}
/**
* The object passed into every plugin to control a command's behavior
*
* @param emitter Any external event emitter.
* The object will be stored in a map, and then fetched by the name of the instance's class provided.
* As there are infinite possibilities to adding external event emitters,
* Most types aren't provided and are as narrow as possibly can.
* @example
* ```
* Sern.addExternal(new Level())
* ```
* ```
* // events/level.ts
* export default eventModule({
* emitter: 'Level',
* type : EventType.External,
* name: 'error',
* execute(args) {
* console.log(args)
* }
* })
*
*/
export function addExternal<T extends EventEmitter>(emitter: T) {
if (ExternalEventEmitters.has(emitter.constructor.name)) {
throw Error(`${emitter.constructor.name} already exists!`);
}
ExternalEventEmitters.set(emitter.constructor.name, emitter);
}
export const controller = {
next: ok,
stop: err,
next: () => Ok.EMPTY,
stop: () => Err.EMPTY,
};
/**
* 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,
);
const onEvent: EventPlugin[] = [];
const plugins: CommandPlugin[] = [];
for (const pl of mod.plugins ?? []) {
if (pl.type === PluginType.Event) {
onEvent.push(pl);
} else {
plugins.push(pl as CommandPlugin);
}
}
return {
...mod,
onEvent,
plugins,
} as 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,
);
const onEvent: EventModuleEventPluginDefs[EventType][] = [];
const plugins: EventModuleCommandPluginDefs[EventType][] = [];
const hasPlugins = mod.plugins && mod.plugins.length > 0;
if (hasPlugins) {
throw Error(
SernError.NotSupportedYet + `: Plugins on event listeners are not supported yet`,
);
}
return {
...mod,
onEvent,
plugins,
} as EventModule;
}
/**
* @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, {}>);
return useContainer<T>();
}
export abstract class CommandExecutable<Type extends CommandType> {
abstract type: Type;
plugins: CommandPlugin<Type>[] = [];
onEvent: EventPlugin<Type>[] = [];
abstract execute: CommandModuleDefs[Type]['execute'];
}
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
plugins: EventModuleCommandPluginDefs[Type][] = [];
onEvent: EventModuleEventPluginDefs[Type][] = [];
abstract execute: EventModuleDefs[Type]['execute'];
}

View File

@@ -27,7 +27,7 @@ class SernEmitter extends EventEmitter {
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param args the arguments for emitting the eventName
* @param args the arguments for emitting the { eventName }
*/
public override emit<T extends keyof SernEventsMapping>(
eventName: T,

View File

@@ -1,106 +1,161 @@
import type { APIGuildMember } from 'discord-api-types/v10';
import type {
ChatInputCommandInteraction,
Client,
Guild,
GuildMember,
InteractionReplyOptions,
Message,
ReplyMessageOptions,
Snowflake,
MessageReplyOptions,
TextBasedChannel,
User,
} from 'discord.js';
import { Result as Either, Ok as Left, Err as Right } from 'ts-results-es';
import type { ReplyOptions } from '../../types/handler';
import { None, Option, Some } from 'ts-results';
import type { Nullish } from '../../types/handler';
import { ExternallyUsed } from '../utilities/externallyUsed';
import { SernError } from './errors';
function safeUnwrap<T>(res: Either<T, T>) {
return res.val;
function firstSome<T>(...args: Option<T>[]): Nullish<T> {
for (const op of args) {
if (op.some) return op.val;
}
return null;
}
//Could I refactor with Either monad?
/**
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
export default class Context {
private constructor(private ctx: Either<Message, ChatInputCommandInteraction>) {}
private constructor(
private oMsg: Option<Message> = None,
private oInterac: Option<ChatInputCommandInteraction> = None,
) {
this.oMsg = oMsg;
this.oInterac = oInterac;
}
/**
* Getting the Message object. Crashes if module type is
* CommandType.Slash or the event fired in a Both command was
* ChatInputCommandInteraction
*/
@ExternallyUsed
public get message() {
return this.ctx.expect(SernError.MismatchEvent);
return this.oMsg.expect(SernError.MismatchEvent);
}
/**
* Getting the ChatInputCommandInteraction object. Crashes if module type is
* CommandType.Text or the event fired in a Both command was
* Message
*/
@ExternallyUsed
public get interaction() {
return this.ctx.expectErr(SernError.MismatchEvent);
return this.oInterac.expect(SernError.MismatchEvent);
}
@ExternallyUsed
public get id(): Snowflake {
return safeUnwrap(this.ctx.map(m => m.id).mapErr(i => i.id));
return firstSome(
this.oInterac.map(i => i.id),
this.oMsg.map(m => m.id),
)!;
}
public get channel() {
return safeUnwrap(this.ctx.map(m => m.channel).mapErr(i => i.channel));
@ExternallyUsed
public get channel(): Nullish<TextBasedChannel> {
return firstSome(
this.oMsg.map(m => m.channel),
this.oInterac.map(i => i.channel),
);
}
/**
* If context is holding a message, message.author
* else, interaction.user
*/
@ExternallyUsed
public get user(): User {
return safeUnwrap(this.ctx.map(m => m.author).mapErr(i => i.user));
return firstSome(
this.oMsg.map(m => m.author),
this.oInterac.map(i => i.user),
)!;
}
@ExternallyUsed
public get createdTimestamp(): number {
return safeUnwrap(this.ctx.map(m => m.createdTimestamp).mapErr(i => i.createdTimestamp));
return firstSome(
this.oMsg.map(m => m.createdTimestamp),
this.oInterac.map(i => i.createdTimestamp),
)!;
}
public get guild() {
return safeUnwrap(this.ctx.map(m => m.guild).mapErr(i => i.guild));
@ExternallyUsed
public get guild(): Guild {
return firstSome(
this.oMsg.map(m => m.guild),
this.oInterac.map(i => i.guild),
)!;
}
public get guildId() {
return safeUnwrap(this.ctx.map(m => m.guildId).mapErr(i => i.guildId));
@ExternallyUsed
public get guildId(): Snowflake {
return firstSome(
this.oMsg.map(m => m.guildId),
this.oInterac.map(i => i.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));
@ExternallyUsed
public get member(): Nullish<GuildMember | APIGuildMember> {
return firstSome(
this.oMsg.map(m => m.member),
this.oInterac.map(i => i.member),
);
}
@ExternallyUsed
public get client(): Client {
return safeUnwrap(this.ctx.map(m => m.client).mapErr(i => i.client));
return firstSome(
this.oMsg.map(m => m.client),
this.oInterac.map(i => i.client),
)!;
}
@ExternallyUsed
public get inGuild(): boolean {
return safeUnwrap(this.ctx.map(m => m.inGuild()).mapErr(i => i.inGuild()));
}
public isMessage() {
return this.ctx.map(() => true).unwrapOr(false);
}
public isSlash() {
return !this.isMessage();
return firstSome(
this.oMsg.map(m => m.inGuild()),
this.oInterac.map(i => i.inGuild()),
)!;
}
static wrap(wrappable: ChatInputCommandInteraction | Message): Context {
if ('token' in wrappable) {
return new Context(Right(wrappable));
return new Context(None, Some(wrappable));
}
return new Context(Left(wrappable));
return new Context(Some(wrappable), None);
}
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()),
),
);
@ExternallyUsed
public isEmpty() {
return this.oMsg.none && this.oInterac.none;
}
//Make queueable
@ExternallyUsed
public reply(
content: string | Omit<InteractionReplyOptions, 'fetchReply'> | ReplyMessageOptions,
) {
return firstSome(
this.oInterac.map(i => {
return i
.reply(content as string | InteractionReplyOptions)
.then(() => i.fetchReply());
}),
this.oMsg.map(m => {
return m.reply(content as string | ReplyMessageOptions);
}),
)!;
}
}

View File

@@ -1,129 +1,26 @@
/**
* A bitfield that discriminates command modules
* @enum { number }
* @example
* ```ts
* export default commandModule({
* // highlight-next-line
* type : CommandType.Text,
* name : 'a text command'
* execute(message) {
* console.log(message.content)
* }
* })
* ```
* @enum { number };
*/
export enum CommandType {
/**
* The CommandType for text commands
*/
Text = 1,
/**
* The CommandType for slash commands
*/
Slash = 2,
/**
* The CommandType for hybrid commands, text and slash
*/
Both = 3,
/**
* The CommandType for UserContextMenuInteraction commands
*/
CtxUser = 4,
/**
* The CommandType for MessageContextMenuInteraction commands
*/
CtxMsg = 8,
/**
* The CommandType for ButtonInteraction commands
*/
Button = 16,
/**
* The CommandType for StringSelectMenuInteraction commands
*/
StringSelect = 32,
/**
* The CommandType for ModalSubmitInteraction commands
*/
Modal = 64,
/**
* The CommandType for the other SelectMenuInteractions
*/
ChannelSelect = 256,
MentionableSelect = 512,
RoleSelect = 1024,
UserSelect = 2048,
enum CommandType {
Text = 0b00000000001,
Slash = 0b00000000010,
Both = 0b0000011,
MenuUser = 0b00000000100,
MenuMsg = 0b0000001000,
Button = 0b00000010000,
MenuSelect = 0b00000100000,
Modal = 0b00001000000,
}
/**
* A bitfield that discriminates event modules
* @enum { number }
* @example
* ```ts
* export default eventModule({
* //highlight-next-line
* type : EventType.Discord,
* name : 'guildMemberAdd'
* execute(member : GuildMember) {
* console.log(member)
* }
* })
* ```
*/
export enum EventType {
/**
* The EventType for handling discord events
*/
Discord = 1,
/**
* The EventType for handling sern events
*/
Sern = 2,
/**
* The EventType for handling external events.
* Could be for example, `process` events, database events
*/
External = 3,
enum EventType {
Discord = 0b01,
Sern = 0b10,
External = 0b11,
}
/**
* A bitfield that discriminates plugins
* @enum { number }
* @example
* ```ts
* export default function myPlugin() : EventPlugin<CommandType.Text> {
* //highlight-next-line
* type : PluginType.Event,
* execute([ctx, args], controller) {
* return controller.next();
* }
* }
* ```
*/
export enum PluginType {
/**
* The PluginType for CommandPlugins
*/
Command = 1,
/**
* The PluginType for EventPlugins
*/
Event = 2,
}
/**
* @enum { string }
*/
export enum PayloadType {
/**
* The PayloadType for a SernEmitter success event
*/
Success = 'success',
/**
* The PayloadType for a SernEmitter failure event
*/
Failure = 'failure',
/**
* The PayloadType for a SernEmitter warning event
*/
Warning = 'warning',
enum PluginType {
Command = 0b01,
Event = 0b10,
}
export { CommandType, PluginType, EventType };

View File

@@ -1,38 +1,9 @@
/**
* @enum { string }
*/
export enum SernError {
/**
* Throws when registering an invalid module.
* This means it is undefined or an invalid command type was provided
*/
InvalidModuleType = 'Detected an unknown module type',
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
UndefinedModule = `A module could not be detected`,
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
NonValidModuleType = 'Detected an unknown module type',
UndefinedModule = `A module could not be detected at`,
MismatchModule = `A module type mismatched with event emitted!`,
/**
* Unsupported interaction at this moment.
*/
NotSupportedInteraction = `This interaction is not supported.`,
/**
* One plugin called `controller.stop()` (end command execution / loading)
*/
PluginFailure = `A plugin failed to call controller.next()`,
/**
* A crash that occurs when accessing an invalid property of Context
*/
MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
/**
* Unsupported feature attempted to access at this time
*/
NotSupportedYet = `This feature is not supported yet`,
/**
* Required Dependency not found
*/
MissingRequired = `@sern/client is required but was not found`,
}

View File

@@ -1,4 +1,5 @@
import type { SernEventsMapping } from '../../types/handler';
import type { Override, SernEventsMapping } from '../../types/handler';
import type { BaseModule } from './module';
import type {
DiscordEmitterPlugin,
DiscordEventPlugin,
@@ -9,31 +10,36 @@ import type {
} 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 type SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping> =
Override<
BaseModule,
{
name?: T;
type: EventType.Sern;
onEvent: SernEventPlugin[];
plugins: SernEmitterPlugin[];
execute(...args: SernEventsMapping[T]): Awaitable<void | unknown>;
}
>;
export type DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents> = Override<
BaseModule,
{
name?: T;
type: EventType.Discord;
onEvent: DiscordEventPlugin[];
plugins: DiscordEmitterPlugin[];
execute(...args: ClientEvents[T]): Awaitable<void | 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>;
}
export type ExternalEventCommand = Override<
BaseModule,
{
emitter: string;
type: EventType.External;
onEvent: ExternalEventPlugin[];
plugins: ExternalEmitterPlugin[];
execute(...args: unknown[]): Awaitable<void | unknown>;
}
>;

View File

@@ -0,0 +1,212 @@
import type {
ApplicationCommandAttachmentOption,
ApplicationCommandChannelOptionData,
ApplicationCommandChoicesData,
ApplicationCommandNonOptionsData,
ApplicationCommandNumericOptionData,
ApplicationCommandOptionData,
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
ApplicationCommandSubGroupData,
AutocompleteInteraction,
Awaitable,
BaseApplicationCommandOptionsData,
ButtonInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
SelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import type { Args, Override, SlashOptions } from '../../types/handler';
import type { AutocompletePlugin, CommandPlugin, EventPlugin } from '../plugins/plugin';
import type Context from './context';
import { CommandType, EventType, PluginType } from './enums';
import type { DiscordEventCommand, ExternalEventCommand, SernEventCommand } from './events';
export interface BaseModule {
type: CommandType | PluginType;
name?: string;
description?: string;
execute: (ctx: Context, args: Args) => Awaitable<void | unknown>;
}
//possible refactoring types into interfaces and not types
export type TextCommand = Override<
BaseModule,
{
type: CommandType.Text;
onEvent: EventPlugin<CommandType.Text>[]; //maybe allow BothPlugins for this also?
plugins: CommandPlugin[]; //maybe allow BothPlugins for this also?
alias?: string[];
execute: (ctx: Context, args: ['text', string[]]) => Awaitable<void | unknown>;
}
>;
export type SlashCommand = Override<
BaseModule,
{
type: CommandType.Slash;
onEvent: EventPlugin<CommandType.Slash>[]; //maybe allow BothPlugins for this also?
plugins: CommandPlugin[]; //maybe allow BothPlugins for this also?
options?: SernOptionsData[];
execute: (ctx: Context, args: ['slash', SlashOptions]) => Awaitable<void | unknown>;
}
>;
export type BothCommand = Override<
BaseModule,
{
type: CommandType.Both;
onEvent: EventPlugin<CommandType.Both>[];
plugins: CommandPlugin[];
alias?: string[];
options?: SernOptionsData[];
execute: (ctx: Context, args: Args) => Awaitable<void | unknown>;
}
>;
export type ContextMenuUser = Override<
BaseModule,
{
type: CommandType.MenuUser;
onEvent: EventPlugin<CommandType.MenuUser>[];
plugins: CommandPlugin[];
execute: (ctx: UserContextMenuCommandInteraction) => Awaitable<void | unknown>;
}
>;
export type ContextMenuMsg = Override<
BaseModule,
{
type: CommandType.MenuMsg;
onEvent: EventPlugin<CommandType.MenuMsg>[];
plugins: CommandPlugin[];
execute: (ctx: MessageContextMenuCommandInteraction) => Awaitable<void | unknown>;
}
>;
export type ButtonCommand = Override<
BaseModule,
{
type: CommandType.Button;
onEvent: EventPlugin<CommandType.Button>[];
plugins: CommandPlugin[];
execute: (ctx: ButtonInteraction) => Awaitable<void | unknown>;
}
>;
export type SelectMenuCommand = Override<
BaseModule,
{
type: CommandType.MenuSelect;
onEvent: EventPlugin<CommandType.MenuSelect>[];
plugins: CommandPlugin[];
execute: (ctx: SelectMenuInteraction) => Awaitable<void | unknown>;
}
>;
export type ModalSubmitCommand = Override<
BaseModule,
{
type: CommandType.Modal;
onEvent: EventPlugin<CommandType.Modal>[];
plugins: CommandPlugin[];
execute: (ctx: ModalSubmitInteraction) => Awaitable<void | unknown>;
}
>;
// Autocomplete commands are a little different
// They can't have command plugins as they are
// in conjunction with chat input commands
// TODO: possibly in future, allow Autocmp commands in separate files?
export type AutocompleteCommand = Override<
BaseModule,
{
name?: never;
description?: never;
type?: never;
onEvent: AutocompletePlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<void | unknown>;
}
>;
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
| SlashCommand
| BothCommand
| ContextMenuUser
| ContextMenuMsg
| ButtonCommand
| SelectMenuCommand
| ModalSubmitCommand;
export type Module = CommandModule | EventModule;
//https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union
// Explicit Module Definitions for mapping
export type CommandModuleDefs = {
[CommandType.Text]: TextCommand;
[CommandType.Slash]: SlashCommand;
[CommandType.Both]: BothCommand;
[CommandType.MenuMsg]: ContextMenuMsg;
[CommandType.MenuUser]: ContextMenuUser;
[CommandType.Button]: ButtonCommand;
[CommandType.MenuSelect]: SelectMenuCommand;
[CommandType.Modal]: ModalSubmitCommand;
};
export type EventModuleDefs = {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
};
//TODO: support deeply nested Autocomplete
// objective: construct union of ApplicationCommandOptionData change any Autocomplete data
// into Sern autocomplete data.
export type SernAutocompleteData = Override<
BaseApplicationCommandOptionsData,
{
autocomplete: true;
type:
| ApplicationCommandOptionType.String
| ApplicationCommandOptionType.Number
| ApplicationCommandOptionType.Integer;
command: AutocompleteCommand;
}
>;
/**
* Type that just uses SernAutocompleteData and not regular autocomplete
*/
export type BaseOptions =
| ApplicationCommandChoicesData
| ApplicationCommandNonOptionsData
| ApplicationCommandChannelOptionData
| ApplicationCommandNumericOptionData
| ApplicationCommandAttachmentOption
| SernAutocompleteData;
export type SernSubCommandData = Override<
Omit<BaseApplicationCommandOptionsData, 'required'>,
{
type: ApplicationCommandOptionType.Subcommand;
options?: BaseOptions[];
}
>;
export type SernSubCommandGroupData = Override<
Omit<BaseApplicationCommandOptionsData, 'required'>,
{
type: ApplicationCommandOptionType.SubcommandGroup;
options?: SernSubCommandData[];
}
>;
export type SernOptionsData<U extends ApplicationCommandOptionData = ApplicationCommandOptionData> =
U extends ApplicationCommandSubCommandData
? SernSubCommandData
: U extends ApplicationCommandSubGroupData
? SernSubCommandGroupData
: BaseOptions;

View File

@@ -1,25 +0,0 @@
import type { CommandModule } from '../../types/module';
import { ApplicationCommandType, ComponentType } from 'discord.js';
/**
* 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 ModalSubmit = new Map<string, CommandModule>();
readonly TextCommands = new Map<string, 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>(),
};
}

View File

@@ -1,6 +1,28 @@
import Context from './context';
import type {
BothCommand,
Module,
SlashCommand,
TextCommand,
SernOptionsData,
BaseOptions,
SernAutocompleteData,
SernSubCommandData,
SernSubCommandGroupData,
} from './module';
import type Wrapper from './wrapper';
import { ModuleStore } from './moduleStore';
export * from './enums';
export { Context, Wrapper, ModuleStore };
export {
Context,
SlashCommand,
TextCommand,
BothCommand,
Module,
Wrapper,
SernOptionsData,
BaseOptions,
SernAutocompleteData,
SernSubCommandData,
SernSubCommandGroupData,
};

View File

@@ -1,15 +1,25 @@
import type { Dependencies } from '../../types/handler';
import type { Client } from 'discord.js';
import type SernEmitter from '../sernEmitter';
import type { EventModule } from './module';
/**
* An object to be passed into Sern#init() function.
* An object to be passed into Sern.Handler constructor.
* @typedef {object} Wrapper
* @property {readonly Client} client
* @prop { readonly SernEmitter } sernEmitter
* @property {readonly string} defaultPrefix
* @property {readonly string} commands
* @prop { readonly DiscordEvent[] } events
*/
interface Wrapper {
readonly client: Client;
readonly sernEmitter?: SernEmitter;
readonly defaultPrefix?: string;
readonly commands: string;
readonly events?: string;
readonly containerConfig: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
};
readonly events?:
| string
| { mod: EventModule; absPath: string }[]
| (() => { mod: EventModule; absPath: string }[]);
}
export default Wrapper;

View File

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

View File

@@ -0,0 +1,18 @@
/**
* This function denotes usage of decorated method is external
* Also, makes method appear 'used' in IDEs
* @param _target
* @param _propertyKey
* @param _descriptor
* @constructor
*/
export function ExternallyUsed(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_target: unknown,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_propertyKey: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_descriptor: PropertyDescriptor,
) {
return void 0;
}

View File

@@ -1,47 +0,0 @@
import * as Files from './readFile';
import { basename } from 'path';
import { Err, Ok, Result } from 'ts-results-es';
import { Observable, of, switchMap } from 'rxjs';
/**
* A function that returns whatever value is provided.
* Used for singleton in iti
* @param value
*/
export const _const =
<T>(value: T) =>
() =>
value;
/**
* A function that returns another function
* Used for transient in iti
* @param value
*/
export const transient =
<T>(value: T) =>
() =>
_const(value);
export function nameOrFilename(modName: string | undefined, absPath: string) {
return modName ?? Files.fmtFileName(basename(absPath));
}
//function wrappers for empty ok / err
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)) {
t.push(el as T);
} else {
v.push(el as 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,13 +1,29 @@
import type { CommandModuleDefs, EventModule, Module } from '../structures/module';
import type {
Awaitable,
ButtonInteraction,
ChatInputCommandInteraction,
CommandInteraction,
MessageComponentInteraction,
MessageContextMenuCommandInteraction,
SelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import {
AutocompleteInteraction,
Interaction,
InteractionType,
ModalSubmitInteraction,
} from 'discord.js';
import type {
DiscordEventCommand,
ExternalEventCommand,
SernEventCommand,
} from '../structures/events';
import { CommandModule, EventType } from '../..';
import type { AnyModule, CommandModuleDefs, EventModule } from '../../types/module';
import { EventType } from '../..';
export function correctModuleType<T extends keyof CommandModuleDefs>(
plug: AnyModule | undefined,
plug: Module | undefined,
type: T,
): plug is CommandModuleDefs[T] {
// Another way to check if type is equivalent,
@@ -15,12 +31,70 @@ export function correctModuleType<T extends keyof CommandModuleDefs>(
return plug !== undefined && (plug.type & type) !== 0;
}
export function isDiscordEvent(el: EventModule | CommandModule): el is DiscordEventCommand {
export function isChatInputCommand(i: CommandInteraction): i is ChatInputCommandInteraction {
return i.isChatInputCommand();
}
export function isButton(i: MessageComponentInteraction): i is ButtonInteraction {
return i.isButton();
}
export function isSelectMenu(i: MessageComponentInteraction): i is SelectMenuInteraction {
return i.isSelectMenu();
}
export function isMessageCtxMenuCmd(
i: CommandInteraction,
): i is MessageContextMenuCommandInteraction {
return i.isMessageContextMenuCommand();
}
export function isUserContextMenuCmd(
i: CommandInteraction,
): i is UserContextMenuCommandInteraction {
return i.isUserContextMenuCommand();
}
export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction {
return interaction.type === InteractionType.ApplicationCommand;
}
export function isModalSubmit(interaction: Interaction): interaction is ModalSubmitInteraction {
return interaction.type === InteractionType.ModalSubmit;
}
export function isAutocomplete(interaction: Interaction): interaction is AutocompleteInteraction {
return interaction.type === InteractionType.ApplicationCommandAutocomplete;
}
export function isMessageComponent(
interaction: Interaction,
): interaction is MessageComponentInteraction {
return interaction.type === InteractionType.MessageComponent;
}
export function isPromise<T>(promiseLike: Awaitable<T>): promiseLike is PromiseLike<T> {
const keys = new Set(Object.keys(promiseLike));
return keys.has('then') && keys.has('catch');
}
export function isDiscordEvent(el: EventModule): el is DiscordEventCommand {
return el.type === EventType.Discord;
}
export function isSernEvent(el: EventModule | CommandModule): el is SernEventCommand {
export function isSernEvent(el: EventModule): el is SernEventCommand {
return el.type === EventType.Sern;
}
export function isExternalEvent(el: EventModule | CommandModule): el is ExternalEventCommand {
export function isExternalEvent(el: EventModule): el is ExternalEventCommand {
return el.type === EventType.External && 'emitter' in el;
}
// export function isEventPlugin<T extends CommandType>(
// e: CommandModulePlugin<T>,
// ): e is EventPlugin<T> {
// return e.type === PluginType.Event;
// }
// export function isCommandPlugin<T extends CommandType>(
// e: CommandModulePlugin<T>,
// ): e is CommandPlugin<T> {
// return !isEventPlugin(e);
// }

View File

@@ -1,8 +1,35 @@
import { ApplicationCommandType, ComponentType } from 'discord.js';
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { type Observable, from, concatAll } from 'rxjs';
import { from, Observable } from 'rxjs';
import type { Module } from '../structures/module';
import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
import type { Result } from 'ts-results';
import { Err, Ok } from 'ts-results';
import type { EventEmitter } from 'events';
//Maybe move this? this probably doesnt belong in utlities/
export const BothCommands = new Map<string, Module>();
export const ApplicationCommands = {
[ApplicationCommandType.User]: new Map<string, Module>(),
[ApplicationCommandType.Message]: new Map<string, Module>(),
[ApplicationCommandType.ChatInput]: new Map<string, Module>(),
} as { [K in ApplicationCommandType]: Map<string, Module> };
export const MessageCompCommands = {
[ComponentType.Button]: new Map<string, Module>(),
[ComponentType.SelectMenu]: new Map<string, Module>(),
[ComponentType.TextInput]: new Map<string, Module>(),
};
export const TextCommands = {
text: new Map<string, Module>(),
aliases: new Map<string, Module>(),
};
export const ModalSubmitCommands = new Map<string, Module>();
/**
* keeps all external emitters stored here
*/
export const ExternalEventEmitters = new Map<string, EventEmitter>();
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
@@ -36,27 +63,15 @@ export function buildData<T>(commandDir: string): Observable<
SernError
>
> {
const commands = getCommands(commandDir);
return from(
Promise.all(
commands.map(async absPath => {
let mod: T | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
mod = require(absPath).default;
} catch {
mod = (await import(`file:///` + absPath)).default;
}
if (mod === undefined) {
return Err(SernError.UndefinedModule);
}
try {
mod = new (mod as unknown as new () => T)();
} catch {}
getCommands(commandDir).map(absPath => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = <T | undefined>require(absPath).default;
if (mod !== undefined) {
return Ok({ mod, absPath });
}),
),
).pipe(concatAll());
} else return Err(SernError.UndefinedModule);
}),
);
}
export function getCommands(dir: string): string[] {

View File

@@ -1,42 +0,0 @@
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
export default function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData | undefined {
if (options === undefined) return undefined;
const _options = options.slice(); // required to prevent direct mutation of options
let autocompleteData: SernAutocompleteData | undefined;
while (_options.length > 0) {
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;
}
}
}
break;
}
}
return autocompleteData;
}

View File

@@ -1,11 +1,7 @@
import SernEmitter from './handler/sernEmitter';
export { eventModule, commandModule, EventExecutable, CommandExecutable } from './handler/sern';
export { eventModule, commandModule } 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 { SernEmitter };
export { _const as single, transient as many } from './handler/utilities/functions';
export { useContainerRaw } from './handler/dependencies/provider';

View File

@@ -1,12 +1,7 @@
import type { CommandInteractionOptionResolver } from 'discord.js';
import type { PayloadType } from '../handler/structures/enums';
import type { InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { EventEmitter } from 'events';
import type { CommandModule, EventModule, AnyModule } from './module';
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 { CommandModule, EventModule, Module } from '../handler/structures/module';
export type Nullish<T> = T | undefined | null;
// Thanks to @kelsny
export type ParseType<T> = {
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
@@ -16,49 +11,41 @@ export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
// Source: https://dev.to/vborodulin/ts-how-to-override-properties-with-type-intersection-554l
export type Override<T1, T2> = Omit<T1, keyof T2> & T2;
export type DefinitelyDefined<T, K extends keyof T = keyof T> = {
[L in K]-?: T[L] extends Record<string, unknown>
? DefinitelyDefined<T[L], keyof T[L]>
: Required<T>[L];
} & T;
type Reconstruct<T> = T extends Omit<infer O, infer _> ? O & Reconstruct<O> : T;
type IsOptional<T> = {
[K in keyof T]-?: T[K] extends Required<T>[K] ? false : true;
};
/**
* Turns a function with a union of array of args into a single union
* [ T , V , B ] | [ A ] => T | V | B | A
*/
export type SpreadParams<T extends (...args: any) => unknown> = (
args: Parameters<T>[number],
) => unknown;
/**
* 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 DefinedModule = DefinitelyDefined<Module, 'name' | 'description'>;
export type DefinedCommandModule = DefinitelyDefined<CommandModule, 'name' | 'description'>;
export type DefinedEventModule = DefinitelyDefined<EventModule, 'name' | 'description'>;
export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string };
| { type: 'success'; module: Module }
| { type: 'failure'; module: Module | undefined; reason: string | Error };
export type SernEventsMapping = {
'module.register': [Payload];
'module.activate': [Payload];
error: [Payload];
warning: [Payload];
['module.register']: [Payload];
['module.activate']: [Payload];
['error']: [Error | string];
};
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/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];
//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';

View File

@@ -1,216 +0,0 @@
import type {
ApplicationCommandAttachmentOption,
ApplicationCommandChannelOptionData,
ApplicationCommandChoicesData,
ApplicationCommandNonOptionsData,
ApplicationCommandNumericOptionData,
ApplicationCommandOptionData,
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
ApplicationCommandSubGroupData,
AutocompleteInteraction,
Awaitable,
BaseApplicationCommandOptionsData,
ButtonInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
UserContextMenuCommandInteraction,
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
RoleSelectMenuInteraction,
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 { EventType } from '../handler/structures/enums';
import type { UserSelectMenuInteraction } from 'discord.js';
export interface Module {
type?: CommandType | EventType;
name?: string;
description?: string;
execute: (...args: any[]) => 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[];
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[];
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[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
| SlashCommand
| BothCommand
| ContextMenuUser
| ContextMenuMsg
| ButtonCommand
| StringSelectCommand
| MentionableSelectCommand
| UserSelectCommand
| ChannelSelectCommand
| RoleSelectCommand
| ModalSubmitCommand;
export type AnyModule = CommandModule | EventModule;
//https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union
// Explicit Module Definitions for mapping
export type CommandModuleDefs = {
[CommandType.Text]: TextCommand;
[CommandType.Slash]: SlashCommand;
[CommandType.Both]: BothCommand;
[CommandType.CtxMsg]: ContextMenuMsg;
[CommandType.CtxUser]: ContextMenuUser;
[CommandType.Button]: ButtonCommand;
[CommandType.StringSelect]: StringSelectCommand;
[CommandType.RoleSelect]: RoleSelectCommand;
[CommandType.ChannelSelect]: ChannelSelectCommand;
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
};
export type EventModuleDefs = {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
};
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
autocomplete: true;
type:
| ApplicationCommandOptionType.String
| ApplicationCommandOptionType.Number
| ApplicationCommandOptionType.Integer;
command: AutocompleteCommand;
}
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
export type BaseOptions =
| ApplicationCommandChoicesData
| ApplicationCommandNonOptionsData
| ApplicationCommandChannelOptionData
| ApplicationCommandNumericOptionData
| ApplicationCommandAttachmentOption
| SernAutocompleteData;
export interface SernSubCommandData extends BaseApplicationCommandOptionsData {
type: ApplicationCommandOptionType.Subcommand;
required?: never;
options?: BaseOptions[];
}
export interface SernSubCommandGroupData extends BaseApplicationCommandOptionsData {
type: ApplicationCommandOptionType.SubcommandGroup;
required?: never;
options?: SernSubCommandData[];
}
export type SernOptionsData<U extends ApplicationCommandOptionData = ApplicationCommandOptionData> =
U extends ApplicationCommandSubCommandData
? SernSubCommandData
: U extends ApplicationCommandSubGroupData
? SernSubCommandGroupData
: BaseOptions;

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

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"moduleResolution": "node",
"skipLibCheck": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["node_modules", "dist"],
"include": ["src"]
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist/cjs",
"target": "esnext"
}
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm",
"target": "esnext"
}
}

21
tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"resolveJsonModule": true,
"target": "esnext",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"moduleResolution": "node",
"skipLibCheck": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
},
"exclude": ["node_modules", "dist"],
"include": ["src"],
}

View File

@@ -1,38 +0,0 @@
import { defineConfig } from 'tsup';
const shared = {
entry: ['src/index.ts'],
external: ['discord.js'],
platform: 'node',
clean: true,
sourcemap: false,
};
export default defineConfig([
{
format: 'esm',
target: 'node16',
tsconfig: './tsconfig-esm.json',
outDir: './dist/esm',
external: ['discord.js'],
treeshake: true,
outExtension() {
return {
js: '.mjs',
};
},
...shared,
},
{
format: 'cjs',
splitting: false,
external: ['discord.js'],
target: 'node16',
tsconfig: './tsconfig-cjs.json',
outDir: './dist/cjs',
outExtension() {
return {
js: '.cjs',
};
},
...shared,
},
]);