├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── SECURITY.md └── workflows │ ├── dependency-review.yml │ ├── npm-publish-dev.yml │ ├── npm-publish.yml │ ├── release-please.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── fortnite ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── core │ ├── functions.ts │ ├── id.ts │ ├── interfaces.ts │ ├── ioc.ts │ ├── module-loading.ts │ ├── modules.ts │ ├── plugin.ts │ ├── presences.ts │ └── structures │ │ ├── context.ts │ │ ├── default-services.ts │ │ ├── enums.ts │ │ └── result.ts ├── handlers │ ├── event-utils.ts │ ├── interaction.ts │ ├── message.ts │ ├── presence.ts │ ├── ready.ts │ ├── tasks.ts │ └── user-defined-events.ts ├── index.ts ├── sern.ts └── types │ ├── core-modules.ts │ ├── core-plugin.ts │ ├── dependencies.d.ts │ └── utility.ts ├── test ├── autocomp.bench.ts ├── core │ ├── context.test.ts │ ├── contracts.test.ts │ ├── create-plugin.test.ts │ ├── functions.test.ts │ ├── id.test.ts │ ├── module-loading.test.ts │ └── presence.test.ts ├── handlers.test.ts ├── mockules │ ├── !ignd.ts │ ├── !ignored │ │ └── ignored.ts │ ├── failed.ts │ ├── module.ts │ └── ug │ │ └── pass.ts └── setup │ ├── setup-tests.ts │ └── util.ts ├── tsconfig.json └── vitest.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jacoobes 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a [code of conduct](https://github.com/sern-handler/handler/blob/main/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ### Our Responsibilities 20 | 21 | Project maintainers are responsible for clarifying the standards of acceptable 22 | behavior and are expected to take appropriate and fair corrective action in 23 | response to any instances of unacceptable behavior. 24 | 25 | Project maintainers have the right and responsibility to remove, edit, or 26 | reject comments, commits, code, wiki edits, issues, and other contributions 27 | that are not aligned to this Code of Conduct, or to ban temporarily or 28 | permanently any contributor for other behaviors that they deem inappropriate, 29 | threatening, offensive, or harmful. 30 | ### Enforcement 31 | 32 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 33 | reported by contacting the project team at our [discord server](https://discord.com/invite/QgnfxWzrcj). All 34 | complaints will be reviewed and investigated and will result in a response that 35 | is deemed necessary and appropriate to the circumstances. The project team is 36 | obligated to maintain confidentiality with regard to the reporter of an incident. 37 | Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good 40 | faith may face temporary or permanent repercussions as determined by other 41 | members of the project's leadership. 42 | 43 | ### Attribution 44 | 45 | This contributing guidelines is adapted from the [Contributor Covenant][homepage], version 1.4, 46 | available at [http://contributor-covenant.org/version/1/4][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/4/ 50 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: sern 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] Write a descriptive title." 5 | labels: bug 6 | assignees: EvolutionX-10, jacoobes, Murtatrxx 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Versioning** 27 | NodeJS version: 28 | DiscordJS version: 29 | SernHandler version: 30 | Channel: (e.g. beta) 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Sern Community Support 4 | url: https://discord.gg/9w8jzsR48U 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Ask for things that are not in sern 4 | title: "[Feature] Request a feature" 5 | labels: feature 6 | assignees: EvolutionX-10, jacoobes, Murtatrxx 7 | 8 | --- 9 | Request a new feature! 10 | --- 11 | ### Is your proposal related to a problem? 12 | 13 | 17 | 18 | (Write your answer here.) 19 | 20 | ### Describe the solution you'd like 21 | 22 | 25 | 26 | (Describe your proposed solution here.) 27 | 28 | ### Describe alternatives you've considered 29 | 30 | 33 | 34 | (Write your answer here.) 35 | 36 | ### Additional context 37 | 38 | 42 | 43 | (Write your answer here.) 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | 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. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # Checklist: 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] My changes generate no new warnings 23 | - [ ] I have added tests that prove my fix is effective or that my feature works 24 | - [ ] New and existing unit tests pass locally with my changes 25 | - [ ] Any dependent changes have been merged and published in downstream modules 26 | 27 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Project is currently under heavy development but you can try out our [npm package](https://npmjs.com/package/@sern/handler) 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.6.1 | YES | 10 | | 2.6.0 | YES | 11 | | 2.5.3 | YES | 12 | | 2.5.2 | YES | 13 | | 2.5.1 | YES | 14 | | 2.5.0 | YES | 15 | | 2.1.1 | NO | 16 | | 2.1.0 | NO | 17 | | 2.0.0 | NO | 18 | | 1.2.1 | NO | 19 | | 1.2.0 | NO | 20 | | 1.1.0 | NO | 21 | 1.0.1 | NO 22 | 1.0.0 | NO 23 | 1.1.9 @ beta | NO 24 | 1.1.8 @ beta | NO 25 | 1.1.7 @ beta | NO 26 | 1.1.6 @ beta | NO 27 | 1.1.5 @ beta | NO 28 | 1.1.4 @ beta | NO 29 | 1.1.3 @ beta | NO 30 | 1.1.2 @ beta | NO 31 | 1.1.1 @ beta | NO 32 | 1.1.0 @ beta | NO 33 | 1.0.4 @ beta | NO 34 | 1.0.3 @ beta | NO 35 | 1.0.2 @ beta | NO 36 | 1.0.1 @ beta | NO 37 | 1.0.0 @ beta | NO 38 | 0.0.1 @ dev | NO (TRY IT) 39 | 40 | 41 | * Dev versions might include bugs and not supported use stable versions. 42 | 43 | ## Reporting a Vulnerability 44 | 45 | You can report a vulnerability by opening an issue on the [project's GitHub](https://github.com/sern-handler/handler/issues) repository. 46 | 47 | Please provide as much information as possible when reporting a vulnerability. We are looking information for, the affected version, and the steps to reproduce the vulnerability. 48 | 49 | Be patient, we are working on fixing all reported vulnerabilities. 50 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # 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. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2 21 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-dev.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Delivery 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**' 9 | - 'package.json' 10 | 11 | jobs: 12 | Publish: 13 | name: Publishing Dev 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Check out Git repository 18 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3 22 | with: 23 | node-version: 18 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - name: Install Node.js dependencies 27 | run: npm i && npm run build:dev 28 | 29 | - name: Publish to npm 30 | run: | 31 | npm version premajor --preid "dev.$(git rev-parse --verify --short HEAD)" --git-tag-version=false 32 | npm publish --tag dev 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM / Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | # We only publish if the version of sern handler is different. workflow automatically cancels if verson is the same 6 | push: 7 | branches: 8 | - 'main' 9 | jobs: 10 | test-and-publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 14 | - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3 15 | with: 16 | node-version: 18 17 | - run: npm i 18 | - run: npm run build:prod 19 | - uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1 20 | with: 21 | token: ${{ secrets.NPM_TOKEN }} 22 | access: "public" 23 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | 3 | on: 4 | workflow_dispatch: 5 | jobs: 6 | release-please: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3 10 | with: 11 | release-type: node 12 | package-name: release-please-action 13 | bump-patch-for-minor-pre-major: true 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [18.x, 19.x, 20.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - run: npm install 28 | - run: npm run test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # Typedoc raw output 58 | .typedoc 59 | 60 | # Dotenv environment variables file 61 | .env 62 | 63 | # Parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js and nuxt.js build output 67 | .next 68 | .nuxt 69 | 70 | # Vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # TypeScript build output 80 | dist 81 | 82 | # VisualStudio Config file 83 | .vs 84 | 85 | # VSCode settings and cache 86 | .vscode 87 | 88 | # IntelliJ IDEA Config file 89 | .idea/ 90 | 91 | # Yarn files 92 | .yarn/install-state.gz 93 | .yarn/build-state.yml 94 | 95 | .yalc 96 | 97 | yalc.lock 98 | 99 | *.svg 100 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | docs/ 3 | .gitignore 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | .yarn 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # Typedoc raw output 63 | .typedoc 64 | 65 | # Dotenv environment variables file 66 | .env 67 | 68 | # Parcel-bundler cache (https://parceljs.org/) 69 | .cache 70 | 71 | # next.js and nuxt.js build output 72 | .next 73 | .nuxt 74 | 75 | # Vuepress build output 76 | .vuepress/dist 77 | 78 | # Serverless directories 79 | .serverless 80 | 81 | # FuseBox cache 82 | .fusebox/ 83 | 84 | test 85 | # VisualStudio Config file 86 | .vs 87 | 88 | # IntelliJ IDEA Config file 89 | .idea/ 90 | 91 | .prettierignore 92 | 93 | .prettierrc 94 | 95 | jest.config.ts 96 | 97 | .eslintrc 98 | 99 | .gitattributes 100 | 101 | .github/ 102 | 103 | CODE_OF_CONDUCT.md 104 | 105 | babel.config.js 106 | 107 | tsup.config.js 108 | 109 | tsconfig-base.json 110 | 111 | tsconfig-cjs.json 112 | 113 | tsconfig-esm.json 114 | 115 | renovate.json 116 | fortnite 117 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | *.md 3 | dist/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.2.4](https://github.com/sern-handler/handler/compare/v4.2.3...v4.2.4) (2025-03-06) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * flat autocomplete ([#395](https://github.com/sern-handler/handler/issues/395)) ([89d7409](https://github.com/sern-handler/handler/commit/89d74095363befddc3222b9e5c89c35e7c6457b9)) 9 | 10 | ## [4.2.3](https://github.com/sern-handler/handler/compare/v4.2.2...v4.2.3) (2025-03-04) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * autocomplete sdt.module not present ([#393](https://github.com/sern-handler/handler/issues/393)) ([2414992](https://github.com/sern-handler/handler/commit/2414992b73a40065464b20f2d53826c78fcd3a5f)) 16 | 17 | ## [4.2.2](https://github.com/sern-handler/handler/compare/v4.2.1...v4.2.2) (2025-02-03) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * faster autocomplete lookup ([#387](https://github.com/sern-handler/handler/issues/387)) ([974c30f](https://github.com/sern-handler/handler/commit/974c30fa6cccaae7b1c2c3246ffa9eecb6bc7bf9)) 23 | 24 | ## [4.2.1](https://github.com/sern-handler/handler/compare/v4.2.0...v4.2.1) (2025-01-24) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * context-interactions error ([#382](https://github.com/sern-handler/handler/issues/382)) ([a52ad27](https://github.com/sern-handler/handler/commit/a52ad270d843e92db5bf2049d07527eed59d428c)) 30 | 31 | ## [4.2.0](https://github.com/sern-handler/handler/compare/v4.1.1...v4.2.0) (2025-01-18) 32 | 33 | 34 | ### Features 35 | 36 | * 4.2.0 load multiple directories & `handleModuleErrors` ([#378](https://github.com/sern-handler/handler/issues/378)) ([f9e7eaf](https://github.com/sern-handler/handler/commit/f9e7eaf92d22b76d3d02a1bbe8324ca6813f48f8)) 37 | 38 | ## [4.1.1](https://github.com/sern-handler/handler/compare/v4.1.0...v4.1.1) (2025-01-13) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * remove rxjs ([#376](https://github.com/sern-handler/handler/issues/376)) ([59d08ef](https://github.com/sern-handler/handler/commit/59d08ef207c486ce1cf0aba267e6f862838e0dfb)) 44 | * This puts the light back into lightweight (\- 4.1 MB) 45 | 46 | ## [4.1.0](https://github.com/sern-handler/handler/compare/v4.0.3...v4.1.0) (2025-01-06) 47 | 48 | 49 | ### Features 50 | 51 | * moduleinfo-in-eventplugins ([#373](https://github.com/sern-handler/handler/issues/373)) ([220a60e](https://github.com/sern-handler/handler/commit/220a60ecf853df8d288de2533c669562a430c3f9)) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * update github username ([#371](https://github.com/sern-handler/handler/issues/371)) ([55715d5](https://github.com/sern-handler/handler/commit/55715d565990fe686159f3c1eda3754d1262c72c)) 57 | 58 | ## [4.0.3](https://github.com/sern-handler/handler/compare/v4.0.2...v4.0.3) (2024-10-06) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * async presence ([#369](https://github.com/sern-handler/handler/issues/369)) ([eabfb81](https://github.com/sern-handler/handler/commit/eabfb81819b53a4656d8eac6e21cfb488b724a42)) 64 | * fix eventModule typing for Discord events ([#368](https://github.com/sern-handler/handler/issues/368)) ([1789ccb](https://github.com/sern-handler/handler/commit/1789ccb2f22f502f87538fecdb07106ff7110434)) 65 | 66 | ## [4.0.2](https://github.com/sern-handler/handler/compare/v4.0.1...v4.0.2) (2024-08-13) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * type issue ([2106cdc](https://github.com/sern-handler/handler/commit/2106cdc1d033f88b6ee4ccca6754fe7a595a9328)) 72 | 73 | ## [4.0.1](https://github.com/sern-handler/handler/compare/v4.0.0...v4.0.1) (2024-07-19) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * add SDT typings to autocomplete commands ([#363](https://github.com/sern-handler/handler/issues/363)) ([92623d2](https://github.com/sern-handler/handler/commit/92623d2914fb80e31365f06cf896bb37f36fc814)) 79 | 80 | ## [4.0.0](https://github.com/sern-handler/handler/compare/v3.3.4...v4.0.0) (2024-07-18) 81 | 82 | 83 | ### Features 84 | 85 | * v4 ([#361](https://github.com/sern-handler/handler/issues/361)) ([9a8904f](https://github.com/sern-handler/handler/commit/9a8904f5aed4fa36b018ad73bbe58049bae33274)) 86 | 87 | 88 | ### Miscellaneous Chores 89 | 90 | * release 4.0.0 ([dda0e33](https://github.com/sern-handler/handler/commit/dda0e3395b6704862bfd3fda2a201e2cb9b45d2f)) 91 | 92 | ## [3.3.4](https://github.com/sern-handler/handler/compare/v3.3.3...v3.3.4) (2024-03-18) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * sern emitter err ([#358](https://github.com/sern-handler/handler/issues/358)) ([90e55df](https://github.com/sern-handler/handler/commit/90e55dfa1466c91e5da48922251309331921b1ef)) 98 | 99 | ## [3.3.3](https://github.com/sern-handler/handler/compare/v3.3.2...v3.3.3) (2024-02-25) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * rm deprecated class modules, clean up, rm indirection ([#355](https://github.com/sern-handler/handler/issues/355)) ([48f9f6e](https://github.com/sern-handler/handler/commit/48f9f6ec16e650d574bd24dcbb0ed176933bfe17)) 105 | * singleton init not being fired when inserting function ([07b11b3](https://github.com/sern-handler/handler/commit/07b11b357baac0c3c7055c022bc353995c80f766)) 106 | * typings and cleanup ([#356](https://github.com/sern-handler/handler/issues/356)) ([ce8c4bf](https://github.com/sern-handler/handler/commit/ce8c4bf6492b9680fb1c1a530d3e0028f214ad2f)) 107 | 108 | ## [3.3.2](https://github.com/sern-handler/handler/compare/v3.3.1...v3.3.2) (2024-01-08) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * presence feature not working on cjs applications ([#351](https://github.com/sern-handler/handler/issues/351)) ([4f23871](https://github.com/sern-handler/handler/commit/4f2387119acfde036d0d1626553e9050f55627d1)) 114 | 115 | ## [3.3.1](https://github.com/sern-handler/handler/compare/v3.3.0...v3.3.1) (2024-01-07) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * crashing when slash command is used as text command ([#349](https://github.com/sern-handler/handler/issues/349)) ([a359f73](https://github.com/sern-handler/handler/commit/a359f73fa24127a4964d411c8c1c0dfea5edc0f1)) 121 | 122 | 123 | ### Reverts 124 | 125 | * the last commit ([655bb8d](https://github.com/sern-handler/handler/commit/655bb8d35815fe0ce9797d8b169310a07b284ae0)) 126 | 127 | ## [3.3.0](https://github.com/sern-handler/handler/compare/v3.2.1...v3.3.0) (2023-12-27) 128 | 129 | 130 | ### Features 131 | 132 | * presence ([#345](https://github.com/sern-handler/handler/issues/345)) ([7458bef](https://github.com/sern-handler/handler/commit/7458befe8a5900480cd71900df02a8364837dc00)) 133 | 134 | ## [3.2.1](https://github.com/sern-handler/handler/compare/v3.2.0...v3.2.1) (2023-12-21) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * logger swap failing ([daac37c](https://github.com/sern-handler/handler/commit/daac37c28858c42b21042bdcb8141239db634e7d)) 140 | 141 | ## [3.2.0](https://github.com/sern-handler/handler/compare/v3.1.1...v3.2.0) (2023-12-15) 142 | 143 | 144 | ### Miscellaneous Chores 145 | 146 | * release 3.2.0 ([237c853](https://github.com/sern-handler/handler/commit/237c8537c66052309d7e13a7e6e0a4f7995c2558)) 147 | 148 | ## [3.1.1](https://github.com/sern-handler/handler/compare/v3.1.0...v3.1.1) (2023-11-06) 149 | 150 | 151 | ### Bug Fixes 152 | 153 | * queuing events ([fd39858](https://github.com/sern-handler/handler/commit/fd39858636d3038abb6d91021b65c99c488a3d6e)) 154 | * queuing events ([#332](https://github.com/sern-handler/handler/issues/332)) @Benzo-Fury ([#333](https://github.com/sern-handler/handler/issues/333)) ([fd39858](https://github.com/sern-handler/handler/commit/fd39858636d3038abb6d91021b65c99c488a3d6e)) 155 | 156 | ## [3.1.0](https://github.com/sern-handler/handler/compare/v3.0.2...v3.1.0) (2023-09-04) 157 | 158 | 159 | ### Features 160 | 161 | * add guaranteed `channelId` and `userId` getters to `Context` ([#320](https://github.com/sern-handler/handler/issues/320)) ([50253ca](https://github.com/sern-handler/handler/commit/50253ca322e7d6dbd2313139c0187a1028f71109)) 162 | * dispose hooks (deprecate useContainerRaw) ([#323](https://github.com/sern-handler/handler/issues/323)) ([26ccd11](https://github.com/sern-handler/handler/commit/26ccd118ff8cbcde94158a4d09fc0df18da9f254)) 163 | 164 | ## [3.0.2](https://github.com/sern-handler/handler/compare/v3.0.1...v3.0.2) (2023-08-06) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * invalid id for cts, mts, cjs, mjs files, node paths ([#318](https://github.com/sern-handler/handler/issues/318)) ([a7f5ea2](https://github.com/sern-handler/handler/commit/a7f5ea269fb344e221d10dbdc26a1611ffc8138f)) 170 | 171 | ## [3.0.1](https://github.com/sern-handler/handler/compare/v3.0.0...v3.0.1) (2023-08-05) 172 | 173 | 174 | ### Bug Fixes 175 | 176 | * collectors ([4134460](https://github.com/sern-handler/handler/commit/41344608c677b6069c46412f5f16e4337182ca7d)) 177 | 178 | ## [3.0.0](https://github.com/sern-handler/handler/compare/v2.6.3...v3.0.0) (2023-07-29) 179 | 180 | 181 | ### ⚠ BREAKING CHANGES 182 | 183 | * v3 ([#294](https://github.com/sern-handler/handler/issues/294)) 184 | 185 | ### Features 186 | 187 | * v3 ([#294](https://github.com/sern-handler/handler/issues/294)) ([7798e36](https://github.com/sern-handler/handler/commit/7798e36458c7f555d2bcb8a5857a6db47b7211da)) 188 | 189 | 190 | ### Miscellaneous Chores 191 | 192 | * release 3.0.0 ([70cca0d](https://github.com/sern-handler/handler/commit/70cca0dbb01e70b47a8c899b1fc4f43dee5ed8ed)) 193 | 194 | ## [2.6.3](https://github.com/sern-handler/handler/compare/v2.6.2...v2.6.3) (2023-06-17) 195 | 196 | 197 | ### Bug Fixes 198 | 199 | * autocomplete nested option and merge main ([5fdc1ed](https://github.com/sern-handler/handler/commit/5fdc1eda7f4fcc1f94af7eca661660c0edeb3251)) 200 | 201 | ## [2.6.2](https://github.com/sern-handler/handler/compare/v2.6.1...v2.6.2) (2023-04-15) 202 | 203 | 204 | ### Miscellaneous Chores 205 | 206 | * release 2.6.2 ([c1f6906](https://github.com/sern-handler/handler/commit/c1f690633c55ba41db1e035b7c16f9e19c70b385)) 207 | 208 | ## [2.6.1](https://github.com/sern-handler/handler/compare/v2.6.0...v2.6.1) (2023-03-17) 209 | 210 | 211 | ### Miscellaneous Chores 212 | 213 | * release 2.6.1 ([f9609ce](https://github.com/sern-handler/handler/commit/f9609ce6cd777fa0eb595d8c48d57905bbce5966)) 214 | 215 | ## [2.6.0](https://github.com/sern-handler/handler/compare/v2.5.3...v2.6.0) (2023-03-09) 216 | 217 | 218 | ### Features 219 | 220 | * adding pure annotation for better tree shaking ([d20d015](https://github.com/sern-handler/handler/commit/d20d01524b872549da501e21feec147ab204f397)) 221 | 222 | ## [2.5.3](https://github.com/sern-handler/handler/compare/v2.5.2...v2.5.3) (2023-02-16) 223 | 224 | 225 | ### Miscellaneous Chores 226 | 227 | * release 2.5.3 ([ce9a083](https://github.com/sern-handler/handler/commit/ce9a0831a6e47dd38648f34653f0bd89b1d2e48e)) 228 | 229 | ## [2.5.2](https://github.com/sern-handler/handler/compare/v2.5.1...v2.5.2) (2023-02-16) 230 | 231 | 232 | ### Reverts 233 | 234 | * version ([facee79](https://github.com/sern-handler/handler/commit/facee79c904ad663d3c57ce56fb825419fcc12f9)) 235 | 236 | ## [2.5.1](https://github.com/sern-handler/handler/compare/v2.5.0...v2.5.1) (2023-02-12) 237 | 238 | 239 | ### Features 240 | 241 | * Adding my bot to readme ([#210](https://github.com/sern-handler/handler/issues/210)) ([96f4281](https://github.com/sern-handler/handler/commit/96f42811218e4898a47e75a8138ccd452ae2c5c2)) 242 | * Adding the WIP to my bot ([86fa531](https://github.com/sern-handler/handler/commit/86fa531eb620d2ac649bad6decb29d5c55a25445)) 243 | 244 | 245 | ### Bug Fixes 246 | 247 | * autocomplete ([1860b89](https://github.com/sern-handler/handler/commit/1860b898f3a231840e2a8b781e007ef9d6f587a4)) 248 | 249 | 250 | ### Miscellaneous Chores 251 | 252 | * release 2.5.1 ([c78936a](https://github.com/sern-handler/handler/commit/c78936a22574da4af71826f5b5f72f354a4eb06a)) 253 | 254 | ## [2.5.0](https://github.com/sern-handler/handler/compare/v2.1.1...v2.5.0) (2023-01-30) 255 | 256 | 257 | ### ⚠ BREAKING CHANGES 258 | 259 | * simpler plugins ([#193](https://github.com/sern-handler/handler/issues/193)) 260 | 261 | ### Features 262 | 263 | * simpler plugins ([#193](https://github.com/sern-handler/handler/issues/193)) ([33f1446](https://github.com/sern-handler/handler/commit/33f14467ec413e003a82503c8a77cb42a6194281)) 264 | 265 | 266 | ### Miscellaneous Chores 267 | 268 | * release 2.5.0 ([b4b195d](https://github.com/sern-handler/handler/commit/b4b195dc9586736760d0b78caa8589f3d6131f8a)) 269 | 270 | ## [2.1.1](https://github.com/sern-handler/handler/compare/v2.1.0...v2.1.1) (2022-12-31) 271 | 272 | 273 | ### Bug Fixes 274 | 275 | * modals remapping ([a13df6f](https://github.com/sern-handler/handler/commit/a13df6fb424d256476284da49024dbe56e82baab)) 276 | 277 | ## [2.1.0](https://github.com/sern-handler/handler/compare/v2.0.0...v2.1.0) (2022-12-30) 278 | 279 | 280 | ### Features 281 | 282 | * grammar ([c30aac4](https://github.com/sern-handler/handler/commit/c30aac476cdc2094de34f9f67b5805204cc5e4dd)) 283 | 284 | 285 | ### Bug Fixes 286 | 287 | * multi parameter events ([e986535](https://github.com/sern-handler/handler/commit/e98653593566ef4635493e0c997bd107a7a3a2a2)) 288 | 289 | ## [2.0.0](https://github.com/sern-handler/handler/compare/v1.2.1...v2.0.0) (2022-12-28) 290 | 291 | 292 | ### ⚠ BREAKING CHANGES 293 | 294 | * (2.0 global services) ([#156](https://github.com/sern-handler/handler/issues/156)) 295 | 296 | ### Features 297 | 298 | * (2.0 global services) ([#156](https://github.com/sern-handler/handler/issues/156)) ([1455622](https://github.com/sern-handler/handler/commit/14556223fd6f79b797fb2aee03e795d4f4e94a8b)) 299 | 300 | ## [1.2.1](https://github.com/sern-handler/handler/compare/v1.2.0...v1.2.1) (2022-10-03) 301 | 302 | 303 | ### Bug Fixes 304 | 305 | * **autocomplete:** now support multiple autocomplete options ([#147](https://github.com/sern-handler/handler/issues/147)) ([cbad738](https://github.com/sern-handler/handler/commit/cbad7380e1993b96c643f365726457f63e4fbd5d)) 306 | 307 | ## [1.2.0](https://github.com/sern-handler/handler/compare/v1.1.0...v1.2.0) (2022-09-28) 308 | 309 | 310 | ### Features 311 | 312 | * allow constructable modules ([#133](https://github.com/sern-handler/handler/issues/133)) ([03936eb](https://github.com/sern-handler/handler/commit/03936eb2ea1d1af7cada04d77bb8345d63a5e20f)) 313 | * classmodules@arcs ([#143](https://github.com/sern-handler/handler/issues/143)) ([5028886](https://github.com/sern-handler/handler/commit/50288867a5b171511941a1be3877d721694e9f77)) 314 | * update CODEOWNERS ([6b8995d](https://github.com/sern-handler/handler/commit/6b8995d149c857558415a6c151a3f575ec373445)) 315 | 316 | 317 | ### Reverts 318 | 319 | * feat of allow constructable modules ([#138](https://github.com/sern-handler/handler/issues/138)) ([82bbdda](https://github.com/sern-handler/handler/commit/82bbddac8d656b60b3a1fb2471ea03ee5224f5c3)) 320 | 321 | ## [1.1.0](https://github.com/sern-handler/handler/compare/v1.0.0...v1.1.0) (2022-08-29) 322 | 323 | 324 | ### Features 325 | 326 | * add proper error handling ([#115](https://github.com/sern-handler/handler/issues/115)) ([395549c](https://github.com/sern-handler/handler/commit/395549c173cb62a18205e451bf2cb5579ba9a6e0)) 327 | 328 | 329 | ### Miscellaneous Chores 330 | 331 | * release 1.1.0 ([8a373de](https://github.com/sern-handler/handler/commit/8a373de880ff18df85af812adf9f6f6a4f45028d)) 332 | 333 | ## 1.0.0 (2022-08-15) 334 | 335 | 336 | ### ⚠ BREAKING CHANGES 337 | 338 | * improve quality of code, refactorings, QOL intellisense (#64) 339 | 340 | ### Features 341 | 342 | * add .prettierignore and ignore README.md ([7ae5ecf](https://github.com/sern-handler/handler/commit/7ae5ecf1a64700d667e85420ae4b2eaf31781d85)) 343 | * Add castings for res ([2697e35](https://github.com/sern-handler/handler/commit/2697e35b2e5b754ea9d0d84db3720fb68b3f43db)) 344 | * Add DefinetlyDefined type, more todo statements ([c8c0c84](https://github.com/sern-handler/handler/commit/c8c0c841db2423e29d69bbc1a3ab590bfebb5d5b)) 345 | * add discord.js as a peerDependency instead ([b3ed8da](https://github.com/sern-handler/handler/commit/b3ed8da68f55b69a7fe1697cd88c552243cc637f)) 346 | * add docs/ to npmignore ([f90342d](https://github.com/sern-handler/handler/commit/f90342d6b140241f7a6a95dea71c05bf309a7a52)) 347 | * add externallyUsed.ts and support BothCommands again ([fc81bfc](https://github.com/sern-handler/handler/commit/fc81bfc6d75e4722486766715abe7271ad21cd7f)) 348 | * add feature-request.md ([#92](https://github.com/sern-handler/handler/issues/92)) ([0d6e592](https://github.com/sern-handler/handler/commit/0d6e592614f0d4eeaaa9ffe5ba245fe002f5b907)) 349 | * add frontmatter ([#95](https://github.com/sern-handler/handler/issues/95)) ([75a6a04](https://github.com/sern-handler/handler/commit/75a6a04db56551049387e38979bb7ef21356f303)) 350 | * Add messageComponent handler ([d29298c](https://github.com/sern-handler/handler/commit/d29298c17a1d67146bdddb9cf07a16924c55ed3a)) 351 | * add version.txt ([4fea383](https://github.com/sern-handler/handler/commit/4fea383519b9905c17c7679587f69b530c08cec8)) 352 | * added conventional commits ([741cf13](https://github.com/sern-handler/handler/commit/741cf13fd56ac49ebca6f73ecc3a2209f00e774d)) 353 | * Added SECURITY file & formatted some TypeScript code ([779011a](https://github.com/sern-handler/handler/commit/779011a124ab76bbfb19a2a11889bf9255cbd360)) 354 | * adding better typings, refactoring ([99e2a99](https://github.com/sern-handler/handler/commit/99e2a997edaac1ba880e56bf782ecd1fa5e96b4c)) 355 | * Adding docs to some data structures, moving to default from export files ([0ae541d](https://github.com/sern-handler/handler/commit/0ae541daba4c5d2aa3e612ab4b78fd6a858717ad)) 356 | * adding modal and autocomplete support ([77856ce](https://github.com/sern-handler/handler/commit/77856ce5d0d8d1e2e2f5a971269224a4174bc205)) 357 | * adding refactoring for repetitive event plugin processing ([475b073](https://github.com/sern-handler/handler/commit/475b0736d573bb8969b2a0eb9180231aa8618a0e)) 358 | * Adding sern event listeners, overriding and typing methods ([115d1a4](https://github.com/sern-handler/handler/commit/115d1a49b52eb45d9b68ba015f8f734b902e9a60)) 359 | * Adding TextInput map & starting event plugins for message components ([6ac9720](https://github.com/sern-handler/handler/commit/6ac9720260040afb12d232b002c28db99b18e093)) 360 | * Aliases optional ([430315a](https://github.com/sern-handler/handler/commit/430315ad02060121e75604aee40c246c71a7e8d2)) 361 | * better looking typings for modules ([53bc080](https://github.com/sern-handler/handler/commit/53bc080a290fd5064993aa0d98497d4b239ac8f3)) 362 | * broadening EventPlugin default generic type, reformat with prettier ([88dcdee](https://github.com/sern-handler/handler/commit/88dcdee818e42405234ef502087226a8c042c92f)) 363 | * CodeQL ([7012da6](https://github.com/sern-handler/handler/commit/7012da60530c2b0b5d8cc97b417a80cd8031f51f)) 364 | * delete partition.ts ([f6d584c](https://github.com/sern-handler/handler/commit/f6d584cf99abdb292985f812e64553a37ab51a01)) 365 | * Edited event names for more conciseness, finished basic event emitters ([3f64a8a](https://github.com/sern-handler/handler/commit/3f64a8aa0a47a09f822d54f2b3f03bc42faa10f7)) 366 | * finished interactionCreate.ts handling? (need test) ([97907b7](https://github.com/sern-handler/handler/commit/97907b746fc94d6e8b65e2fec1cce4b0c3160491)) 367 | * finishing autocomplete!! ([d63423c](https://github.com/sern-handler/handler/commit/d63423cfc458cb9ab07b9900a7c4d2f7ea8d71b9)) 368 | * finishing optionData for autocomplete changes, adding class for builder ([b08eebf](https://github.com/sern-handler/handler/commit/b08eebf6850acaee3b56bb1c60aec2a026a5144c)) 369 | * Finishing up autocomplete, need to test ([d50b801](https://github.com/sern-handler/handler/commit/d50b8013ee343b2be0ed232938e9f5f91c43b493)) 370 | * fix duplicate ([c5bd941](https://github.com/sern-handler/handler/commit/c5bd94131dfb20b2c69b7eeb96f3ad89d6de43f4)) 371 | * **handler:** adding higher-order-function wrappers to increase readability and shorten code ([0f0b0fb](https://github.com/sern-handler/handler/commit/0f0b0fb61c80654179e2c6d6f69e50c70114201b)) 372 | * **handler:** command plugins work?! ([70bd12d](https://github.com/sern-handler/handler/commit/70bd12dd61182f48445c707a9199421b1dba586e)) 373 | * **handler:** progress on event plugins ([2f61399](https://github.com/sern-handler/handler/commit/2f61399b5e5ad53ee3165e19cb74dd279b827b99)) 374 | * **higherorders.ts:** a new function that acts as a command options builder ([651009c](https://github.com/sern-handler/handler/commit/651009c9ed5e8e04cf44fa4438f94a9e119aa8f8)) 375 | * 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)) 376 | * Improved TypeScript experince ([dad3042](https://github.com/sern-handler/handler/commit/dad3042a644b0e47d01319f48eefe01632678cc3)) 377 | * interactionCreate.ts refactoring ([c4e8e51](https://github.com/sern-handler/handler/commit/c4e8e517b3f4bb6baca902251a0afa22b2548450)) 378 | * Making name required in auto cmp interactions ([ac8a2f4](https://github.com/sern-handler/handler/commit/ac8a2f4c86a1c426d32e388a5439a8696db52279)) 379 | * move name and description out of OptionsData[] ([93942bd](https://github.com/sern-handler/handler/commit/93942bd0e7d0ac688d20159cab2c70c3285085f4)) 380 | * Optional plugins to reduce bloat ([2b81d53](https://github.com/sern-handler/handler/commit/2b81d53503209a738b70d238512c82542d3d88e8)) 381 | * **prefix:** make defaultPrefix optional ([f6b88dc](https://github.com/sern-handler/handler/commit/f6b88dcdc80c407e14f4d6ae73eb27e75d195e18)) 382 | * **readme.md:** added basic command examples ([63b2d3a](https://github.com/sern-handler/handler/commit/63b2d3a5723ac6e1f0baa0de8b65640cecd7d634)) 383 | * remove comments about prev commit ([a220949](https://github.com/sern-handler/handler/commit/a2209494bdd05ca89176aff82f7d3afce0b8de46)) 384 | * remove copyright bloat ([48737ef](https://github.com/sern-handler/handler/commit/48737efea3c0fce572238701e72335293333379f)) 385 | * remove externallyUsed.ts ([3dec347](https://github.com/sern-handler/handler/commit/3dec347ef0957845601f0eb2acb3f1815d1e9ca1)) 386 | * Revamp Docs ([#47](https://github.com/sern-handler/handler/issues/47)) ([163e48f](https://github.com/sern-handler/handler/commit/163e48f3eb38d37500cefc8d0c722c083a3070c7)) 387 | * **sern.ts:** adding logging instead of large complaining messages from bot ([00a5fa4](https://github.com/sern-handler/handler/commit/00a5fa43ad9e0b4c7d5ef1f2772a4cb186768837)) 388 | * **sern.ts:** beginning to add new basic logger system ([ef9d53e](https://github.com/sern-handler/handler/commit/ef9d53e6b1a9009eab5ce9ff9f8b5542d1d7cf7f)) 389 | * **sern.ts:** changed how module is passed around, now has name property at runtime ([40fb723](https://github.com/sern-handler/handler/commit/40fb7231436331c97fa791eab3b8b51636e826f1)) 390 | * **sern.ts:** changing default value of args in text based cmd to string[], from string ([1397974](https://github.com/sern-handler/handler/commit/1397974fb6e6d8c1b1e82db8272ef0a57916022c)) 391 | * **sern.ts:** changing default value of args in text based cmd to string[], from string ([e0541f7](https://github.com/sern-handler/handler/commit/e0541f777bc3dcb1ec0c0cccf219b9fa66199a2b)) 392 | * **sern.ts:** changing text-based command parser fn value to string[] ([b11f999](https://github.com/sern-handler/handler/commit/b11f9996749977a16e516523af7a8e2a9e6521ae)) 393 | * **sern.ts:** renaming Module.delegate to Module.execute ([8702876](https://github.com/sern-handler/handler/commit/870287674aa7eccbe1fc890d1cf2cd808982b801)) 394 | * should be able to register other nodejs event emitters ([b8cda35](https://github.com/sern-handler/handler/commit/b8cda351e1f549422692c633255ac1d6c7d78a9b)) 395 | * 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)) 396 | * update example ([0da1b5a](https://github.com/sern-handler/handler/commit/0da1b5a4dc6823807880ade03728b466fe895190)) 397 | * Updated Readme design ([369586f](https://github.com/sern-handler/handler/commit/369586f378f807ba9906167b5ada197c2c95e411)) 398 | 399 | 400 | ### Bug Fixes 401 | 402 | * accidentally imported wildcard from wrong place & namespace ([8782cad](https://github.com/sern-handler/handler/commit/8782cad9cddbb24c03c2bfff96d3377aceb5f542)) 403 | * autocomplete in nested form ([#97](https://github.com/sern-handler/handler/issues/97)) ([70d7bdb](https://github.com/sern-handler/handler/commit/70d7bdb8c53a1990addc5c9fd54427f194833b4e)) 404 | * Change discord server link ([#62](https://github.com/sern-handler/handler/issues/62)) ([e677ce0](https://github.com/sern-handler/handler/commit/e677ce083966dedc945d236034e2ce4a7a586e05)) 405 | * **codeql-analysis.yml:** Fixed autobuild issue on some TS files & deleted more unused comments ([e51c7ff](https://github.com/sern-handler/handler/commit/e51c7ffed038f0519a37f4339406c28546d92c83)) 406 | * crash on collectors ([#89](https://github.com/sern-handler/handler/issues/89)) ([a0587f5](https://github.com/sern-handler/handler/commit/a0587f59d43d62642c033e0bb843902f9e6dc0c4)) 407 | * crash on collectors pt ([7da7bff](https://github.com/sern-handler/handler/commit/7da7bff700f8e46e72412ca5d379a6edbc14e10a)) 408 | * didn't run prettier, now i am ([6c144de](https://github.com/sern-handler/handler/commit/6c144defcacd7732e15292f6c3e5eaea7944bc32)) 409 | * Fix return type of sernModule ([cf85a5d](https://github.com/sern-handler/handler/commit/cf85a5db6413e2b8b42143f70964f7a19789e1ff)) 410 | * Fixed renovate warning ([#77](https://github.com/sern-handler/handler/issues/77)) ([76c4333](https://github.com/sern-handler/handler/commit/76c4333a817006100f0b99d73bb455e82797d3d9)) 411 | * Fixed typo in Security.md ([c35def9](https://github.com/sern-handler/handler/commit/c35def99c93e77a0c932a1b8f1da06cd45fde294)) 412 | * **handler.ts:** added the changes of Module.execute to type delegate (now type execute) ([f062a33](https://github.com/sern-handler/handler/commit/f062a338687be4b3ac64c048a63bdcb895282d2d)) 413 | * linting issue in markup.ts ([dac665d](https://github.com/sern-handler/handler/commit/dac665d6281a29ec79663adb26a3e5c5243e6ae0)) 414 | * Non-exhaustiveness led to commands not registering readyEvent.ts ([b266508](https://github.com/sern-handler/handler/commit/b26650818e2c193c326356359b38412117ea6186)) 415 | * prettier changes again ([d5bb992](https://github.com/sern-handler/handler/commit/d5bb9922dfdb14e4f7e95ad5acd470765b7a90c2)) 416 | * prettier wants lf line ending ([571a804](https://github.com/sern-handler/handler/commit/571a8044b027afee91466219a841817dd55ef455)) 417 | * **sern.ts:** checking ctx instanceof Message always returned false ([7166947](https://github.com/sern-handler/handler/commit/7166947d28f3be1a6e1c44385eb7946731784f58)) 418 | * Standard for of does not resolve promises. Switched to for await ([66b9f51](https://github.com/sern-handler/handler/commit/66b9f51fa73cf07a589671d13ca4c65a9c8193a1)) 419 | * **utilexports.ts:** forgot to remove the deleted feat of option builder ([1cff46c](https://github.com/sern-handler/handler/commit/1cff46c0ab5959d8e0f0fe89f1e6cd4c6cebff19)) 420 | 421 | 422 | ### Reverts 423 | 424 | * Move enums to enums.ts ([40a10bf](https://github.com/sern-handler/handler/commit/40a10bf32b9a1c6afbf85bdaeb2a7918c15ac7a8)) 425 | * Re-add plugins overload ([b9b5919](https://github.com/sern-handler/handler/commit/b9b59197df7d9bbeac3df68ebe6f7cea1ce50677)) 426 | * remove version.txt ([ca9ac52](https://github.com/sern-handler/handler/commit/ca9ac52fae32108b4cb90b201204d5c358c5ef7b)) 427 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 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. 3 | 4 | # The Pledge 5 | 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. 6 | 7 | # The Standards 8 | Examples of behaviour that contributes to creating a positive environment include: 9 | 10 | * Using welcoming and inclusive language 11 | * Being respectful of differing viewpoints and experiences 12 | * Gracefully accepting constructive criticism 13 | * Referring to people by their preferred pronouns and using gender-neutral pronouns when uncertain 14 | * Examples of unacceptable behaviour by participants include: 15 | 16 | • Trolling, insulting/derogatory comments, public or private harassment
17 | • Publishing others' private information, such as a physical or electronic address, without explicit permission
18 | • Not being respectful to reasonable communication boundaries, such as 'leave me alone,' 'go away,' or 'I’m not discussing this with you.'
19 | • The usage of sexualised language or imagery and unwelcome sexual attention or advances
20 | • Swearing, usage of strong or disturbing language
21 | • Demonstrating the graphics or any other content you know may be considered disturbing
22 | • Starting and/or participating in arguments related to politics
23 | • Assuming or promoting any kind of inequality including but not limited to: age, body size, disability, ethnicity, gender identity and expression, nationality and race, personal appearance, religion, or sexual identity and orientation
24 | • Drug promotion of any kind
25 | • Attacking personal tastes
26 | • Other conduct which you know could reasonably be considered inappropriate in a professional setting.
27 | • Enforcement.
28 | 29 | 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. 30 | 31 | 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. 32 | 33 | # Attribution 34 | This Code of Conduct is adapted from [dev.to](https://dev.to). 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 sern 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

Handlers. Redefined.

6 |

A complete, customizable, typesafe, & reactive framework for discord bots

7 | 8 |
9 | 10 | 11 | NPM version 12 | NPM downloads 13 | License MIT 14 | docs.rs 15 | Lines of code 16 |
17 | 18 | ## Why? 19 | - For you. A framework that's tailored to your exact needs. 20 | - Lightweight. Does a lot while being small. 21 | - Latest features. Support for discord.js v14 and all of its interactions. 22 | - Start quickly. Plug and play or customize to your liking. 23 | - Works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box! 24 | - Use it with TypeScript or JavaScript. CommonJS and ESM supported. 25 | - Active and growing community, always here to help. [Join us](https://sern.dev/discord) 26 | - Unleash its full potential with a powerful CLI and awesome plugins. 27 | 28 | ## 📜 Installation 29 | [Start here!!](https://sern.dev/v4/reference/getting-started) 30 | 31 | ## 👶 Basic Usage 32 |
ping.ts 33 | 34 | ```ts 35 | export default commandModule({ 36 | type: CommandType.Slash, 37 | //Installed plugin to publish to discord api and allow access to owners only. 38 | plugins: [publish(), ownerOnly()], 39 | description: 'A ping pong command', 40 | execute(ctx) { 41 | ctx.reply('Hello owner of the bot'); 42 | } 43 | }); 44 | ``` 45 |
46 | 47 | # Show off your sern Discord Bot! 48 | 49 | ## Badge 50 | - Copy this and add it to your [README.md](https://img.shields.io/badge/built_with-sern-pink?labelColor=%230C3478&color=%23ed5087&link=https%3A%2F%2Fsern.dev) 51 | 52 | 53 | ## 🤖 Bots Using sern 54 | - [Community Bot](https://github.com/sern-handler/sern-community) - The community bot for our [Discord server](https://sern.dev/discord). 55 | - [Vinci](https://github.com/SrIzan10/vinci) - The bot for Mara Turing. 56 | - [Bask](https://github.com/baskbotml/bask) - Listen to your favorite artists on Discord. 57 | - [Murayama](https://github.com/murayamabot/murayama) - :pepega: 58 | - [Protector](https://github.com/GlitchApotamus/Protector) - Just a simple bot to help enhance a private Minecraft server. 59 | - [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud) - A fun bot for a small, but growing server. 60 | - [Man Nomic](https://github.com/jacoobes/man-nomic) - A simple information bot to provide information to the nomic-ai Discord community. 61 | - [Linear-Discord](https://github.com/sern-handler/linear-discord) - Display and manage a linear dashboard. 62 | - [ZenithBot](https://github.com/CodeCraftersHaven/ZenithBot) - A versatile bot coded in TypeScript, designed to enhance server management and user interaction through its robust features. 63 | 64 | ## 💻 CLI 65 | 66 | 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. 67 | 68 | 69 | 70 | ## 🔗 Links 71 | 72 | - [Official Documentation and Guide](https://sern.dev) 73 | - [Support Server](https://sern.dev/discord) 74 | 75 | ## 👋 Contribute 76 | - Read our contribution [guidelines](https://github.com/sern-handler/handler/blob/main/.github/CONTRIBUTING.md) carefully 77 | - Pull up on [issues](https://github.com/sern-handler/handler/issues) and report bugs 78 | - All kinds of contributions are welcomed. 79 | 80 | -------------------------------------------------------------------------------- /fortnite: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sern/handler", 3 | "packageManager": "yarn@3.5.0", 4 | "version": "4.2.4", 5 | "description": "A complete, customizable, typesafe, & reactive framework for discord bots.", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.js" 12 | } 13 | }, 14 | "scripts": { 15 | "watch": "tsc --watch", 16 | "lint": "eslint src/**/*.ts", 17 | "format": "eslint src/**/*.ts --fix", 18 | "build:dev": "tsc", 19 | "build:prod": "tsc", 20 | "prepare": "tsc", 21 | "pretty": "prettier --write .", 22 | "tdd": "vitest", 23 | "benchmark": "vitest bench", 24 | "test": "vitest --run", 25 | "analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg" 26 | }, 27 | "keywords": [ 28 | "sern-handler", 29 | "sern", 30 | "handler", 31 | "sern handler", 32 | "wrapper", 33 | "discord.js", 34 | "framework" 35 | ], 36 | "author": "SernDevs", 37 | "license": "MIT", 38 | "dependencies": { 39 | "@sern/ioc": "^1.1.2", 40 | "callsites": "^3.1.0", 41 | "cron": "^3.1.7", 42 | "deepmerge": "^4.3.1" 43 | }, 44 | "devDependencies": { 45 | "@faker-js/faker": "^8.0.1", 46 | "@types/node": "^20.0.0", 47 | "@types/node-cron": "^3.0.11", 48 | "@typescript-eslint/eslint-plugin": "5.58.0", 49 | "@typescript-eslint/parser": "5.59.1", 50 | "discord.js": "^14.14.1", 51 | "eslint": "8.39.0", 52 | "typescript": "5.0.2", 53 | "vitest": "^1.6.0" 54 | }, 55 | "eslintConfig": { 56 | "parser": "@typescript-eslint/parser", 57 | "extends": [ 58 | "plugin:@typescript-eslint/recommended" 59 | ], 60 | "parserOptions": { 61 | "ecmaVersion": "latest", 62 | "sourceType": "script" 63 | }, 64 | "rules": { 65 | "@typescript-eslint/no-non-null-assertion": "off", 66 | "quotes": [ 67 | 2, 68 | "single", 69 | { 70 | "avoidEscape": true, 71 | "allowTemplateLiterals": true 72 | } 73 | ], 74 | "semi": [ 75 | "error", 76 | "always" 77 | ], 78 | "@typescript-eslint/no-empty-interface": 0, 79 | "@typescript-eslint/ban-types": 0, 80 | "@typescript-eslint/no-explicit-any": "off" 81 | } 82 | }, 83 | "repository": { 84 | "type": "git", 85 | "url": "git+https://github.com/sern-handler/handler.git" 86 | }, 87 | "engines": { 88 | "node": ">= 20.0.x" 89 | }, 90 | "homepage": "https://sern.dev", 91 | "overrides": { 92 | "ws": "8.17.1" 93 | }, 94 | "resolutions": { 95 | "ws": "8.17.1" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "helpers:pinGitHubActionDigests", 5 | "group:allNonMajor" 6 | ], 7 | "major": { 8 | "dependencyDashboardApproval": true, 9 | "reviewers": ["EvolutionX-10", "jacoobes", "Murtatrxx"] 10 | }, 11 | "minor": { 12 | "reviewers": ["jacoobes", "Murtatrxx"] 13 | }, 14 | "schedule": ["every weekend"], 15 | "lockFileMaintenance": { 16 | "enabled": true, 17 | "automerge": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/functions.ts: -------------------------------------------------------------------------------- 1 | import type { Module, SernAutocompleteData, SernOptionsData } from '../types/core-modules'; 2 | import type { 3 | AnySelectMenuInteraction, 4 | ButtonInteraction, 5 | ChatInputCommandInteraction, 6 | MessageContextMenuCommandInteraction, 7 | ModalSubmitInteraction, 8 | UserContextMenuCommandInteraction, 9 | AutocompleteInteraction, 10 | } from 'discord.js'; 11 | import { ApplicationCommandOptionType, InteractionType } from 'discord.js'; 12 | import { PluginType } from './structures/enums'; 13 | import type { Payload, UnpackedDependencies } from '../types/utility'; 14 | import path from 'node:path' 15 | 16 | export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => { 17 | return { 18 | state: {}, 19 | deps, 20 | params, 21 | type: module.type, 22 | module: { 23 | name: module.name, 24 | description: module.description, 25 | locals: module.locals, 26 | meta: module.meta 27 | } 28 | } 29 | } 30 | 31 | /** 32 | * Removes the first character(s) _[depending on prefix length]_ of the message 33 | * @param msg 34 | * @param prefix The prefix to remove 35 | * @returns The message without the prefix 36 | * @example 37 | * message.content = '!ping'; 38 | * console.log(fmt(message.content, '!')); 39 | * // [ 'ping' ] 40 | */ 41 | export function fmt(msg: string, prefix?: string): string[] { 42 | if(!prefix) throw Error("Unable to parse message without prefix"); 43 | return msg.slice(prefix.length).trim().split(/\s+/g); 44 | } 45 | 46 | 47 | export function partitionPlugins 48 | (arr: Array<{ type: PluginType }> = []): [T[], V[]] { 49 | const controlPlugins = []; 50 | const initPlugins = []; 51 | for (const el of arr) { 52 | switch (el.type) { 53 | case PluginType.Control: controlPlugins.push(el); break; 54 | case PluginType.Init: initPlugins.push(el); break; 55 | } 56 | } 57 | return [controlPlugins, initPlugins] as [T[], V[]]; 58 | } 59 | 60 | export const createLookupTable = (options: SernOptionsData[]): Map => { 61 | const table = new Map(); 62 | _createLookupTable(table, options, ""); 63 | return table; 64 | } 65 | 66 | const _createLookupTable = (table: Map, options: SernOptionsData[], parent: string) => { 67 | for (const opt of options) { 68 | const name = path.posix.join(parent, opt.name) 69 | switch(opt.type) { 70 | case ApplicationCommandOptionType.Subcommand: { 71 | _createLookupTable(table, opt.options ?? [], name); 72 | } break; 73 | case ApplicationCommandOptionType.SubcommandGroup: { 74 | _createLookupTable(table, opt.options ?? [], name); 75 | } break; 76 | default: { 77 | if(Reflect.get(opt, 'autocomplete') === true) { 78 | table.set(name, opt as SernAutocompleteData) 79 | } 80 | } break; 81 | } 82 | } 83 | 84 | } 85 | 86 | interface InteractionTypable { 87 | type: InteractionType; 88 | } 89 | //discord.js pls fix ur typings or i will >:( 90 | type AnyMessageComponentInteraction = AnySelectMenuInteraction | ButtonInteraction; 91 | type AnyCommandInteraction = 92 | | ChatInputCommandInteraction 93 | | MessageContextMenuCommandInteraction 94 | | UserContextMenuCommandInteraction; 95 | 96 | export function isMessageComponent(i: InteractionTypable): i is AnyMessageComponentInteraction { 97 | return i.type === InteractionType.MessageComponent; 98 | } 99 | export function isCommand(i: InteractionTypable): i is AnyCommandInteraction { 100 | return i.type === InteractionType.ApplicationCommand; 101 | } 102 | export function isContextCommand(i: AnyCommandInteraction): i is MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction { 103 | return i.isContextMenuCommand(); 104 | } 105 | export function isAutocomplete(i: InteractionTypable): i is AutocompleteInteraction { 106 | return i.type === InteractionType.ApplicationCommandAutocomplete; 107 | } 108 | 109 | export function isModal(i: InteractionTypable): i is ModalSubmitInteraction { 110 | return i.type === InteractionType.ModalSubmit; 111 | } 112 | 113 | export function resultPayload 114 | (type: T, module?: Module, reason?: unknown) { 115 | return { type, module, reason } as Payload & { type : T }; 116 | } 117 | 118 | export function pipe(arg: unknown, firstFn: Function, ...fns: Function[]): T { 119 | let result = firstFn(arg); 120 | for (let fn of fns) { 121 | result = fn(result); 122 | } 123 | return result; 124 | } 125 | -------------------------------------------------------------------------------- /src/core/id.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationCommandType, ComponentType, type Interaction, InteractionType } from 'discord.js'; 2 | import { CommandType, EventType } from './structures/enums'; 3 | 4 | const parseParams = (event: { customId: string }, append: string) => { 5 | const hasSlash = event.customId.indexOf('/') 6 | if(hasSlash === -1) { 7 | return { id:event.customId+append }; 8 | } 9 | const baseid = event.customId.substring(0, hasSlash); 10 | const params = event.customId.substring(hasSlash+1); 11 | return { id: baseid+append, params } 12 | } 13 | /** 14 | * Construct unique ID for a given interaction object. 15 | * @param event The interaction object for which to create an ID. 16 | * @returns An array of unique string IDs based on the type and properties of the interaction object. 17 | */ 18 | export function reconstruct(event: T) { 19 | switch (event.type) { 20 | case InteractionType.MessageComponent: { 21 | const data = parseParams(event, `_C${event.componentType}`) 22 | return [data]; 23 | } 24 | case InteractionType.ApplicationCommand: 25 | case InteractionType.ApplicationCommandAutocomplete: 26 | return [{ id: `${event.commandName}_A${event.commandType}` }, { id: `${event.commandName}_B` }]; 27 | //Modal interactions are classified as components for sern 28 | case InteractionType.ModalSubmit: { 29 | const data = parseParams(event, '_M'); 30 | return [data]; 31 | } 32 | } 33 | } 34 | /** 35 | * 36 | * A magic number to represent any commandtype that is an ApplicationCommand. 37 | */ 38 | const PUBLISHABLE = 0b000000001111; 39 | 40 | 41 | const TypeMap = new Map([[CommandType.Text, 0], 42 | [CommandType.Both, 0], 43 | [CommandType.Slash, ApplicationCommandType.ChatInput], 44 | [CommandType.CtxUser, ApplicationCommandType.User], 45 | [CommandType.CtxMsg, ApplicationCommandType.Message], 46 | [CommandType.Button, ComponentType.Button], 47 | [CommandType.StringSelect, ComponentType.StringSelect], 48 | [CommandType.Modal, InteractionType.ModalSubmit], 49 | [CommandType.UserSelect, ComponentType.UserSelect], 50 | [CommandType.MentionableSelect, ComponentType.MentionableSelect], 51 | [CommandType.RoleSelect, ComponentType.RoleSelect], 52 | [CommandType.ChannelSelect, ComponentType.ChannelSelect]]); 53 | 54 | /* 55 | * Generates an id based on name and CommandType. 56 | * A is for any ApplicationCommand. C is for any ComponentCommand 57 | * Then, another number fetched from TypeMap 58 | */ 59 | export function create(name: string, type: CommandType | EventType) { 60 | if(type == CommandType.Text) { 61 | return `${name}_T`; 62 | } 63 | if(type == CommandType.Both) { 64 | return `${name}_B`; 65 | } 66 | if(type == CommandType.Modal) { 67 | return `${name}_M`; 68 | } 69 | const am = (PUBLISHABLE & type) !== 0 ? 'A' : 'C'; 70 | return `${name}_${am}${TypeMap.get(type)!}` 71 | } 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/core/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { AnyFunction } from '../types/utility'; 2 | 3 | 4 | /** 5 | * Represents an initialization contract. 6 | * Let dependencies implement this to initiate some logic. 7 | */ 8 | export interface Init { 9 | init(): unknown; 10 | } 11 | 12 | /** 13 | * Represents a Disposable contract. 14 | * Let dependencies implement this to dispose and cleanup. 15 | */ 16 | export interface Disposable { 17 | dispose(): unknown; 18 | } 19 | 20 | 21 | export interface Emitter { 22 | addListener(eventName: string | symbol, listener: AnyFunction): this; 23 | removeListener(eventName: string | symbol, listener: AnyFunction): this; 24 | emit(eventName: string | symbol, ...payload: any[]): boolean; 25 | } 26 | 27 | 28 | /** 29 | * @since 2.0.0 30 | */ 31 | export interface ErrorHandling { 32 | /** 33 | * @deprecated 34 | * Version 4 will remove this method 35 | */ 36 | crash(err: Error): never; 37 | /** 38 | * A function that is called on every throw. 39 | * @param error 40 | */ 41 | updateAlive(error: Error): void; 42 | 43 | } 44 | 45 | /** 46 | * @since 2.0.0 47 | */ 48 | export interface Logging { 49 | error(payload: LogPayload): void; 50 | warning(payload: LogPayload): void; 51 | info(payload: LogPayload): void; 52 | debug(payload: LogPayload): void; 53 | } 54 | 55 | export type LogPayload = { message: T }; 56 | -------------------------------------------------------------------------------- /src/core/ioc.ts: -------------------------------------------------------------------------------- 1 | import { Service as $Service, Services as $Services } from '@sern/ioc/global' 2 | import { Container } from '@sern/ioc'; 3 | import * as Contracts from './interfaces'; 4 | import * as __Services from './structures/default-services'; 5 | import type { Logging } from './interfaces'; 6 | import { __init_container, useContainerRaw } from '@sern/ioc/global'; 7 | import { EventEmitter } from 'node:events'; 8 | import { Client } from 'discord.js'; 9 | import { Module } from '../types/core-modules'; 10 | import { UnpackFunction } from '../types/utility'; 11 | 12 | export function disposeAll(logger: Logging|undefined) { 13 | useContainerRaw() 14 | ?.disposeAll() 15 | .then(() => logger?.info({ message: 'Cleaning container and crashing' })); 16 | } 17 | 18 | type Insertable = | ((container: Dependencies) => object) 19 | | object 20 | 21 | const dependencyBuilder = (container: Container) => { 22 | return { 23 | /** 24 | * Insert a dependency into your container. 25 | * Supply the correct key and dependency 26 | */ 27 | add(key: keyof Dependencies, v: Insertable) { 28 | if(typeof v !== 'function') { 29 | container.addSingleton(key, v) 30 | } else { 31 | //@ts-ignore 32 | container.addWiredSingleton(key, (cntr) => v(cntr)) 33 | } 34 | }, 35 | /** 36 | * @param key the key of the dependency 37 | * @param v The dependency to swap out. 38 | * Swap out a preexisting dependency. 39 | */ 40 | swap(key: keyof Dependencies, v: Insertable) { 41 | if(typeof v !== 'function') { 42 | container.swap(key, v); 43 | } else { 44 | container.swap(key, v(container.deps())); 45 | } 46 | }, 47 | }; 48 | }; 49 | 50 | type ValidDependencyConfig = 51 | (c: ReturnType) => any 52 | 53 | /** 54 | * makeDependencies constructs a dependency injection container for sern handler to use. 55 | * This is required to start the handler, and is to be called before Sern.init. 56 | * @example 57 | * ```ts 58 | * await makeDependencies(({ add }) => { 59 | * add('@sern/client', new Client({ intents, partials }) 60 | * }) 61 | * ``` 62 | */ 63 | export async function makeDependencies (conf: ValidDependencyConfig) { 64 | const container = await __init_container({ autowire: false }); 65 | //We only include logger if it does not exist 66 | const includeLogger = !container.hasKey('@sern/logger'); 67 | 68 | if(includeLogger) { 69 | container.addSingleton('@sern/logger', new __Services.DefaultLogging); 70 | } 71 | container.addSingleton('@sern/errors', new __Services.DefaultErrorHandling); 72 | container.addSingleton('@sern/modules', new Map); 73 | container.addSingleton('@sern/emitter', new EventEmitter({ captureRejections: true })) 74 | container.addSingleton('@sern/scheduler', new __Services.TaskScheduler) 75 | conf(dependencyBuilder(container)); 76 | await container.ready(); 77 | } 78 | 79 | 80 | /** 81 | * The Service api, which allows users to access dependencies in places IOC cannot reach. 82 | * To obtain intellisense, ensure a .d.ts file exists in the root of compilation. 83 | * Our scaffolding tool takes care of this. 84 | * Note: this method only works AFTER your container has been initiated 85 | * @since 3.0.0 86 | * @example 87 | * ```ts 88 | * const client = Service('@sern/client'); 89 | * ``` 90 | * @param key a key that corresponds to a dependency registered. 91 | * @throws if container is absent or not present 92 | */ 93 | export function Service(key: T) { 94 | return $Service(key) as Dependencies[T] 95 | } 96 | /** 97 | * @since 3.0.0 98 | * The plural version of {@link Service} 99 | * @throws if container is absent or not present 100 | * @returns array of dependencies, in the same order of keys provided 101 | * 102 | */ 103 | export function Services(...keys: [...T]) { 104 | return $Services>(...keys) 105 | } 106 | 107 | /** 108 | * @deprecated 109 | * Creates a singleton object. 110 | * @param cb 111 | */ 112 | export function single(cb: () => T) { 113 | console.log('The `single` function is deprecated and has no effect') 114 | return cb(); 115 | } 116 | 117 | /** 118 | * @deprecated 119 | * @since 2.0.0 120 | * Creates a transient object 121 | * @param cb 122 | */ 123 | export function transient(cb: () => () => T) { 124 | console.log('The `transient` function is deprecated and has no effect') 125 | return cb()(); 126 | } 127 | 128 | export type DependencyFromKey = Dependencies[T]; 129 | 130 | 131 | 132 | export type IntoDependencies = { 133 | [Index in keyof Tuple]: UnpackFunction>>; //Unpack and make NonNullable 134 | } & { length: Tuple['length'] }; 135 | 136 | export interface CoreDependencies { 137 | /** 138 | * discord.js client. 139 | */ 140 | '@sern/client': Client; 141 | /** 142 | * sern emitter listens to events that happen throughout 143 | * the handler. some include module.register, module.activate. 144 | */ 145 | '@sern/emitter': Contracts.Emitter; 146 | /** 147 | * An error handler which is the final step before 148 | * the sern process actually crashes. 149 | */ 150 | '@sern/errors': Contracts.ErrorHandling; 151 | /** 152 | * Optional logger. Performs ... logging 153 | */ 154 | '@sern/logger'?: Contracts.Logging; 155 | /** 156 | * Readonly module store. sern stores these 157 | * by module.meta.id -> Module 158 | */ 159 | '@sern/modules': Map; 160 | 161 | '@sern/scheduler': __Services.TaskScheduler 162 | } 163 | 164 | -------------------------------------------------------------------------------- /src/core/module-loading.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { existsSync } from 'node:fs'; 3 | import { readdir } from 'fs/promises'; 4 | import assert from 'node:assert'; 5 | import * as Id from './id' 6 | import { Module } from '../types/core-modules'; 7 | 8 | export const parseCallsite = (site: string) => { 9 | const pathobj = path.posix.parse(site.replace(/file:\\?/, "") 10 | .split(path.sep) 11 | .join(path.posix.sep)) 12 | return { name: pathobj.name, 13 | absPath : path.posix.format(pathobj) } 14 | } 15 | 16 | export const shouldHandle = (pth: string, filenam: string) => { 17 | const file_name = filenam+path.extname(pth); 18 | let newPath = path.join(path.dirname(pth), file_name) 19 | .replace(/file:\\?/, ""); 20 | return { exists: existsSync(newPath), 21 | path: 'file://'+newPath }; 22 | } 23 | 24 | 25 | /** 26 | * Import any module based on the absolute path. 27 | * This can accept four types of exported modules 28 | * commonjs, javascript : 29 | * ```js 30 | * exports = commandModule({ }) 31 | * //or 32 | * exports.default = commandModule({ }) 33 | * ``` 34 | * esm javascript, typescript, and commonjs typescript 35 | * export default commandModule({}) 36 | */ 37 | export async function importModule(absPath: string) { 38 | let fileModule = await import(absPath); 39 | 40 | let commandModule: Module = fileModule.default; 41 | 42 | assert(commandModule , `No default export @ ${absPath}`); 43 | if ('default' in commandModule) { 44 | commandModule = commandModule.default as Module; 45 | } 46 | const p = path.parse(absPath) 47 | commandModule.name ??= p.name; commandModule.description ??= "..."; 48 | commandModule.meta = { 49 | id: Id.create(commandModule.name, commandModule.type), 50 | absPath, 51 | }; 52 | return { module: commandModule as T }; 53 | } 54 | 55 | 56 | export async function* readRecursive(dir: string): AsyncGenerator { 57 | const files = await readdir(dir, { withFileTypes: true }); 58 | for (const file of files) { 59 | const fullPath = path.posix.join(dir, file.name); 60 | if (file.isDirectory()) { 61 | if (!file.name.startsWith('!')) { 62 | yield* readRecursive(fullPath); 63 | } 64 | } else if (!file.name.startsWith('!')) { 65 | yield "file:///"+path.resolve(fullPath); 66 | } 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/core/modules.ts: -------------------------------------------------------------------------------- 1 | import type { ClientEvents } from 'discord.js'; 2 | import { EventType } from '../core/structures/enums'; 3 | import type { 4 | InputCommand, 5 | InputEvent, 6 | Module, 7 | ScheduledTask, 8 | } from '../types/core-modules'; 9 | import { partitionPlugins } from './functions' 10 | import type { Awaitable } from '../types/utility'; 11 | 12 | /** 13 | * Creates a command module with standardized structure and plugin support. 14 | * 15 | * @since 1.0.0 16 | * @param {InputCommand} mod - Command module configuration 17 | * @returns {Module} Processed command module ready for registration 18 | * 19 | * @example 20 | * // Basic slash command 21 | * export default commandModule({ 22 | * type: CommandType.Slash, 23 | * description: "Ping command", 24 | * execute: async (ctx) => { 25 | * await ctx.reply("Pong! 🏓"); 26 | * } 27 | * }); 28 | * 29 | * @example 30 | * // Command with component interaction 31 | * export default commandModule({ 32 | * type: CommandType.Slash, 33 | * description: "Interactive command", 34 | * execute: async (ctx) => { 35 | * const button = new ButtonBuilder({ 36 | * customId: "btn/someData", 37 | * label: "Click me", 38 | * style: ButtonStyle.Primary 39 | * }); 40 | * await ctx.reply({ 41 | * content: "Interactive message", 42 | * components: [new ActionRowBuilder().addComponents(button)] 43 | * }); 44 | * } 45 | * }); 46 | */ 47 | export function commandModule(mod: InputCommand): Module { 48 | const [onEvent, plugins] = partitionPlugins(mod.plugins); 49 | return { ...mod, 50 | onEvent, 51 | plugins, 52 | locals: {} } as Module; 53 | } 54 | 55 | 56 | /** 57 | * Creates an event module for handling Discord.js or custom events. 58 | * 59 | * @since 1.0.0 60 | * @template T - Event name from ClientEvents 61 | * @param {InputEvent} mod - Event module configuration 62 | * @returns {Module} Processed event module ready for registration 63 | * @throws {Error} If ControlPlugins are used in event modules 64 | * 65 | * @example 66 | * // Discord event listener 67 | * export default eventModule({ 68 | * type: EventType.Discord, 69 | * execute: async (message) => { 70 | * console.log(`${message.author.tag}: ${message.content}`); 71 | * } 72 | * }); 73 | * 74 | * @example 75 | * // Custom sern event 76 | * export default eventModule({ 77 | * type: EventType.Sern, 78 | * execute: async (eventData) => { 79 | * // Handle sern-specific event 80 | * } 81 | * }); 82 | */ 83 | export function eventModule(mod: InputEvent): Module { 84 | const [onEvent, plugins] = partitionPlugins(mod.plugins); 85 | if(onEvent.length !== 0) throw Error("Event modules cannot have ControlPlugins"); 86 | return { ...mod, 87 | plugins, 88 | locals: {} } as Module; 89 | } 90 | 91 | /** Create event modules from discord.js client events, 92 | * This was an {@link eventModule} for discord events, 93 | * where typings were bad. 94 | * @deprecated Use {@link eventModule} instead 95 | * @param mod 96 | */ 97 | export function discordEvent(mod: { 98 | name: T; 99 | once?: boolean; 100 | execute: (...args: ClientEvents[T]) => Awaitable; 101 | }) { 102 | return eventModule({ type: EventType.Discord, ...mod, }); 103 | } 104 | 105 | /** 106 | * Creates a scheduled task that can be executed at specified intervals using cron patterns 107 | * 108 | * @param {ScheduledTask} ism - The scheduled task configuration object 109 | * @param {string} ism.trigger - A cron pattern that determines when the task should execute 110 | * Format: "* * * * *" (minute hour day month day-of-week) 111 | * @param {Function} ism.execute - The function to execute when the task is triggered 112 | * @param {Object} ism.execute.context - The execution context passed to the task 113 | * 114 | * @returns {ScheduledTask} The configured scheduled task 115 | * 116 | * @example 117 | * // Create a task that runs every minute 118 | * export default scheduledTask({ 119 | * trigger: "* * * * *", 120 | * execute: (context) => { 121 | * console.log("Task executed!"); 122 | * } 123 | * }); 124 | * 125 | * @remarks 126 | * - Tasks must be placed in the 'tasks' directory specified in your config 127 | * - The file name serves as a unique identifier for the task 128 | * - Tasks can be cancelled using deps['@sern/scheduler'].kill(uuid) 129 | * 130 | * @see {@link https://crontab.guru/} for testing and creating cron patterns 131 | */ 132 | export function scheduledTask(ism: ScheduledTask): ScheduledTask { 133 | return ism 134 | } 135 | 136 | -------------------------------------------------------------------------------- /src/core/plugin.ts: -------------------------------------------------------------------------------- 1 | import { CommandType, PluginType } from './structures/enums'; 2 | import type { Plugin, PluginResult, CommandArgs, InitArgs } from '../types/core-plugin'; 3 | import { Err, Ok } from './structures/result'; 4 | import type { Dictionary } from '../types/utility'; 5 | 6 | export function makePlugin( 7 | type: PluginType, 8 | execute: (...args: any[]) => any, 9 | ): Plugin { 10 | return { type, execute } as Plugin; 11 | } 12 | /** 13 | * @since 2.5.0 14 | */ 15 | export function EventInitPlugin(execute: (args: InitArgs) => PluginResult) { 16 | return makePlugin(PluginType.Init, execute); 17 | } 18 | 19 | /** 20 | * Creates an initialization plugin for command preprocessing and modification 21 | * 22 | * @since 2.5.0 23 | * @template I - Extends CommandType to enforce type safety for command modules 24 | * 25 | * @param {function} execute - Function to execute during command initialization 26 | * @param {InitArgs} execute.args - The initialization arguments 27 | * @param {T} execute.args.module - The command module being initialized 28 | * @param {string} execute.args.absPath - The absolute path to the module file 29 | * @param {Dependencies} execute.args.deps - Dependency injection container 30 | * 31 | * @returns {Plugin} A plugin that runs during command initialization 32 | * 33 | * @example 34 | * // Plugin to update command description 35 | * export const updateDescription = (description: string) => { 36 | * return CommandInitPlugin(({ deps }) => { 37 | * if(description.length > 100) { 38 | * deps.logger?.info({ message: "Invalid description" }) 39 | * return controller.stop("From updateDescription: description is invalid"); 40 | * } 41 | * module.description = description; 42 | * return controller.next(); 43 | * }); 44 | * }; 45 | * 46 | * @example 47 | * // Plugin to store registration date in module locals 48 | * export const dateRegistered = () => { 49 | * return CommandInitPlugin(({ module }) => { 50 | * module.locals.registered = Date.now() 51 | * return controller.next(); 52 | * }); 53 | * }; 54 | * 55 | * @remarks 56 | * - Init plugins can modify how commands are loaded and perform preprocessing 57 | * - The module.locals object can be used to store custom plugin-specific data 58 | * - Be careful when modifying module fields as multiple plugins may interact with them 59 | * - Use controller.next() to continue to the next plugin 60 | * - Use controller.stop(reason) to halt plugin execution 61 | */ 62 | export function CommandInitPlugin( 63 | execute: (args: InitArgs) => PluginResult 64 | ): Plugin { 65 | return makePlugin(PluginType.Init, execute); 66 | } 67 | 68 | /** 69 | * Creates a control plugin for command preprocessing, filtering, and state management 70 | * 71 | * @since 2.5.0 72 | * @template I - Extends CommandType to enforce type safety for command modules 73 | * 74 | * @param {function} execute - Function to execute during command control flow 75 | * @param {CommandArgs} execute.args - The command arguments array 76 | * @param {Context} execute.args[0] - The discord context (e.g., guild, channel, user info, interaction) 77 | * @param {SDT} execute.args[1] - The State, Dependencies, Params, Module, and Type object 78 | * 79 | * @returns {Plugin} A plugin that runs during command execution flow 80 | * 81 | * @example 82 | * // Plugin to restrict command to specific guild 83 | * export const inGuild = (guildId: string) => { 84 | * return CommandControlPlugin((ctx, sdt) => { 85 | * if(ctx.guild.id !== guildId) { 86 | * return controller.stop(); 87 | * } 88 | * return controller.next(); 89 | * }); 90 | * }; 91 | * 92 | * @example 93 | * // Plugins passing state through the chain 94 | * const plugin1 = CommandControlPlugin((ctx, sdt) => { 95 | * return controller.next({ 'plugin1/data': 'from plugin1' }); 96 | * }); 97 | * 98 | * const plugin2 = CommandControlPlugin((ctx, sdt) => { 99 | * return controller.next({ 'plugin2/data': ctx.user.id }); 100 | * }); 101 | * 102 | * export default commandModule({ 103 | * type: CommandType.Slash, 104 | * plugins: [plugin1, plugin2], 105 | * execute: (ctx, sdt) => { 106 | * console.log(sdt.state); // Access accumulated state 107 | * } 108 | * }); 109 | * 110 | * @remarks 111 | * - Control plugins are executed in order when a discord.js event is emitted 112 | * - Use controller.next() to continue to next plugin or controller.stop() to halt execution 113 | * - State can be passed between plugins using controller.next({ key: value }) 114 | * - State keys should be namespaced to avoid collisions (e.g., 'plugin-name/key') 115 | * - Final accumulated state is passed to the command's execute function 116 | * - All plugins must succeed for the command to execute 117 | * - Plugins have access to dependencies through the sdt.deps object 118 | * - Useful for implementing preconditions, filters, and command preprocessing 119 | */ 120 | export function CommandControlPlugin( 121 | execute: (...args: CommandArgs) => PluginResult, 122 | ) { 123 | return makePlugin(PluginType.Control, execute); 124 | } 125 | 126 | 127 | /** 128 | * @since 1.0.0 129 | * The object passed into every plugin to control a command's behavior 130 | */ 131 | export const controller = { 132 | next: (val?: Dictionary) => Ok(val), 133 | stop: (val?: string) => Err(val), 134 | }; 135 | 136 | 137 | export type Controller = typeof controller; 138 | -------------------------------------------------------------------------------- /src/core/presences.ts: -------------------------------------------------------------------------------- 1 | import type { ActivitiesOptions } from "discord.js"; 2 | import type { IntoDependencies } from "./ioc"; 3 | import type { Emitter } from "./interfaces"; 4 | import { Awaitable } from "../types/utility"; 5 | 6 | type Status = 'online' | 'idle' | 'invisible' | 'dnd' 7 | type PresenceReduce = (previous: Presence.Result) => Awaitable; 8 | 9 | export const Presence = { 10 | /** 11 | * A small wrapper to provide type inference. 12 | * Create a Presence module which **MUST** be put in a file called presence.(language-extension) 13 | * adjacent to the file where **Sern.init** is CALLED. 14 | */ 15 | module : (conf: Presence.Config) => conf, 16 | /** 17 | * Create a Presence body which can be either: 18 | * - once, the presence is activated only once. 19 | * - repeated, per cycle or event, the presence can be changed. 20 | */ 21 | of : (root: Omit) => { 22 | return { 23 | /** 24 | * @example 25 | * Presence 26 | * .of({ activities: [{ name: "deez nuts" }] }) //starts presence with "deez nuts". 27 | * .repeated(prev => { 28 | * return { 29 | * afk: true, 30 | * activities: prev.activities?.map(s => ({ ...s, name: s.name+"s" })) 31 | * }; 32 | * }, 10000)) //every 10 s, the callback sets the presence to the value returned. 33 | */ 34 | repeated: (onRepeat: PresenceReduce, repeat: number | [Emitter, string]) => { 35 | return { repeat, onRepeat, ...root } 36 | }, 37 | /** 38 | * @example 39 | * ```ts 40 | * Presence.of({ 41 | * activities: [{ name: "Chilling out" }] 42 | * }).once() // Sets the presence once, with what's provided in '.of()' 43 | * ``` 44 | */ 45 | once: () => root 46 | }; 47 | } 48 | } 49 | export declare namespace Presence { 50 | export type Config = { 51 | inject?: [...T] 52 | execute: (...v: IntoDependencies) => Awaitable; 53 | 54 | } 55 | 56 | export interface Result { 57 | status?: Status; 58 | afk?: boolean; 59 | activities?: ActivitiesOptions[]; 60 | shardId?: number[]; 61 | repeat?: number | [Emitter, string]; 62 | onRepeat?: PresenceReduce 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/core/structures/context.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BaseInteraction, 3 | ChatInputCommandInteraction, 4 | Client, 5 | InteractionReplyOptions, 6 | Message, 7 | MessageReplyOptions, 8 | Snowflake, 9 | User, 10 | } from 'discord.js'; 11 | import { Result, Ok, Err, val } from './result'; 12 | import * as assert from 'assert'; 13 | import type { ReplyOptions } from '../../types/utility'; 14 | import { fmt } from '../functions' 15 | import { SernError } from './enums'; 16 | 17 | 18 | /** 19 | * @since 1.0.0 20 | * Provides values shared between 21 | * Message and ChatInputCommandInteraction 22 | */ 23 | export class Context { 24 | 25 | get options() { 26 | if(this.isMessage()) { 27 | const [, ...rest] = fmt(this.message.content, this.prefix); 28 | return rest; 29 | } 30 | return this.interaction.options; 31 | } 32 | 33 | 34 | protected constructor(protected ctx: Result, 35 | private __prefix?: string) { } 36 | public get prefix() { 37 | return this.__prefix; 38 | } 39 | public get id(): Snowflake { 40 | return val(this.ctx).id 41 | } 42 | 43 | public get channel() { 44 | return val(this.ctx).channel; 45 | } 46 | 47 | public get channelId(): Snowflake { 48 | return val(this.ctx).channelId; 49 | } 50 | 51 | /** 52 | * If context is holding a message, message.author 53 | * else, interaction.user 54 | */ 55 | public get user(): User { 56 | if(this.ctx.ok) { 57 | return this.ctx.value.author; 58 | } 59 | return this.ctx.error.user; 60 | 61 | } 62 | 63 | public get userId(): Snowflake { 64 | return this.user.id; 65 | } 66 | 67 | public get createdTimestamp(): number { 68 | return val(this.ctx).createdTimestamp; 69 | } 70 | 71 | public get guild() { 72 | return val(this.ctx).guild; 73 | } 74 | 75 | public get guildId() { 76 | return val(this.ctx).guildId; 77 | } 78 | /* 79 | * interactions can return APIGuildMember if the guild it is emitted from is not cached 80 | */ 81 | public get member() { 82 | return val(this.ctx).member; 83 | } 84 | 85 | get message(): Message { 86 | if(this.ctx.ok) { 87 | return this.ctx.value; 88 | } 89 | throw Error(SernError.MismatchEvent); 90 | } 91 | public isMessage(): this is Context & { ctx: Result } { 92 | return this.ctx.ok; 93 | } 94 | 95 | public isSlash(): this is Context & { ctx: Result } { 96 | return !this.isMessage(); 97 | } 98 | 99 | get interaction(): ChatInputCommandInteraction { 100 | if(!this.ctx.ok) { 101 | return this.ctx.error; 102 | } 103 | throw Error(SernError.MismatchEvent); 104 | } 105 | 106 | 107 | public get client(): Client { 108 | return val(this.ctx).client; 109 | } 110 | 111 | public get inGuild(): boolean { 112 | return val(this.ctx).inGuild() 113 | } 114 | 115 | public async reply(content: ReplyOptions) { 116 | if(this.ctx.ok) { 117 | return this.ctx.value.reply(content as MessageReplyOptions) 118 | } 119 | interface FetchReply { fetchReply: true }; 120 | return this.ctx.error.reply(content as InteractionReplyOptions & FetchReply) 121 | 122 | } 123 | 124 | static wrap(wrappable: BaseInteraction | Message, prefix?: string): Context { 125 | if ('interaction' in wrappable) { 126 | return new Context(Ok(wrappable), prefix); 127 | } 128 | assert.ok(wrappable.isChatInputCommand(), "Context created with bad interaction."); 129 | return new Context(Err(wrappable), prefix); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/core/structures/default-services.ts: -------------------------------------------------------------------------------- 1 | import { ScheduledTask } from '../../types/core-modules'; 2 | import type { LogPayload, Logging, ErrorHandling, Disposable } from '../interfaces'; 3 | import { CronJob } from 'cron'; 4 | 5 | /** 6 | * @internal 7 | * @since 2.0.0 8 | * Version 4.0.0 will internalize this api. Please refrain from using the defaults! 9 | */ 10 | export class DefaultErrorHandling implements ErrorHandling { 11 | crash(err: Error): never { 12 | throw err; 13 | } 14 | updateAlive(err: Error) { 15 | throw err; 16 | } 17 | } 18 | 19 | 20 | /** 21 | * @internal 22 | * @since 2.0.0 23 | * Version 4.0.0 will internalize this api. Please refrain from using ModuleStore! 24 | */ 25 | export class DefaultLogging implements Logging { 26 | private date() { return new Date() } 27 | debug(payload: LogPayload): void { 28 | console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`); 29 | } 30 | 31 | error(payload: LogPayload): void { 32 | console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`); 33 | } 34 | 35 | info(payload: LogPayload): void { 36 | console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`); 37 | } 38 | 39 | warning(payload: LogPayload): void { 40 | console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`); 41 | } 42 | } 43 | 44 | 45 | export class TaskScheduler implements Disposable { 46 | private __tasks: Map> = new Map(); 47 | 48 | schedule(uuid: string, task: ScheduledTask, deps: Dependencies) { 49 | if (this.__tasks.has(uuid)) { 50 | throw Error("while scheduling a task \ 51 | found another task of same name. Not scheduling " + 52 | uuid + "again." ); 53 | } 54 | try { 55 | const onTick = async function(this: CronJob) { 56 | task.execute({ id: uuid, 57 | lastTimeExecution: this.lastExecution, 58 | nextTimeExecution: this.nextDate().toJSDate() }, { deps }) 59 | } 60 | const job = CronJob.from({ cronTime: task.trigger, onTick, timeZone: task.timezone }); 61 | job.start(); 62 | this.__tasks.set(uuid, job); 63 | } catch (error) { 64 | throw Error(`while scheduling a task ${uuid} ` + error); 65 | } 66 | } 67 | 68 | kill(taskName: string): boolean { 69 | const job = this.__tasks.get(taskName); 70 | if (job) { 71 | job.stop(); 72 | this.__tasks.delete(taskName); 73 | return true; 74 | } 75 | return false; 76 | } 77 | 78 | get tasks(): string[] { 79 | return Array.from(this.__tasks.keys()); 80 | } 81 | 82 | dispose() { 83 | this.__tasks.forEach((_, id) => { 84 | this.kill(id); 85 | this.__tasks.delete(id); 86 | }) 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/core/structures/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | * A bitfield that discriminates command modules 4 | * @enum { number } 5 | * @example 6 | * ```ts 7 | * export default commandModule({ 8 | * // highlight-next-line 9 | * type : CommandType.Text, 10 | * name : 'a text command' 11 | * execute(message) { 12 | * console.log(message.content) 13 | * } 14 | * }) 15 | * ``` 16 | */ 17 | export enum CommandType { 18 | Text = 1 << 0, 19 | Slash = 1 << 1, 20 | Both = 3, 21 | CtxUser = 1 << 2, 22 | CtxMsg = 1 << 3, 23 | Button = 1 << 4, 24 | StringSelect = 1 << 5, 25 | Modal = 1 << 6, 26 | UserSelect = 1 << 7, 27 | RoleSelect = 1 << 8, 28 | MentionableSelect = 1 << 9, 29 | ChannelSelect = 1 << 10, 30 | } 31 | 32 | /** 33 | * A bitfield that discriminates event modules 34 | * @enum { number } 35 | * @example 36 | * ```ts 37 | * export default eventModule({ 38 | * //highlight-next-line 39 | * type : EventType.Discord, 40 | * name : 'guildMemberAdd' 41 | * execute(member : GuildMember) { 42 | * console.log(member) 43 | * } 44 | * }) 45 | * ``` 46 | */ 47 | export enum EventType { 48 | /** 49 | * The EventType for handling discord events 50 | */ 51 | Discord, 52 | /** 53 | * The EventType for handling sern events 54 | */ 55 | Sern, 56 | /** 57 | * The EventType for handling external events. 58 | * Could be for example, `process` events, database events 59 | */ 60 | External, 61 | } 62 | 63 | /** 64 | * A bitfield that discriminates plugins 65 | * @enum { number } 66 | * @example 67 | * ```ts 68 | * export default function myPlugin() : EventPlugin { 69 | * //highlight-next-line 70 | * type : PluginType.Event, 71 | * execute([ctx, args], controller) { 72 | * return controller.next(); 73 | * } 74 | * } 75 | * ``` 76 | */ 77 | export enum PluginType { 78 | /** 79 | * The PluginType for InitPlugins 80 | */ 81 | Init = 1, 82 | /** 83 | * The PluginType for EventPlugins 84 | */ 85 | Control = 2, 86 | } 87 | /** 88 | * @deprecated - Use strings 'success' | 'failure' | 'warning' 89 | * @enum { string } 90 | */ 91 | export enum PayloadType { 92 | Success = 'success', 93 | Failure = 'failure', 94 | Warning = 'warning', 95 | } 96 | 97 | /** 98 | * @enum { string } 99 | */ 100 | export const enum SernError { 101 | /** 102 | * Throws when registering an invalid module. 103 | * This means it is undefined or an invalid command type was provided 104 | */ 105 | InvalidModuleType = 'Detected an unknown module type', 106 | /** 107 | * Attempted to lookup module in command module store. Nothing was found! 108 | */ 109 | UndefinedModule = `A module could not be detected`, 110 | /** 111 | * Attempted to lookup module in command module store. Nothing was found! 112 | */ 113 | MismatchModule = `A module type mismatched with event emitted!`, 114 | /** 115 | * Unsupported interaction at this moment. 116 | */ 117 | NotSupportedInteraction = `This interaction is not supported.`, 118 | /** 119 | * One plugin called `controller.stop()` (end command execution / loading) 120 | */ 121 | PluginFailure = `A plugin failed to call controller.next()`, 122 | /** 123 | * A crash that occurs when accessing an invalid property of Context 124 | */ 125 | MismatchEvent = `You cannot use message when an interaction fired or vice versa`, 126 | /** 127 | * Unsupported feature attempted to access at this time 128 | */ 129 | NotSupportedYet = `This feature is not supported yet`, 130 | /** 131 | * Required Dependency not found 132 | */ 133 | MissingRequired = `@sern/client is required but was not found`, 134 | } 135 | -------------------------------------------------------------------------------- /src/core/structures/result.ts: -------------------------------------------------------------------------------- 1 | export type Result = 2 | | { ok: true; value: Ok } 3 | | { ok: false; error: Err }; 4 | 5 | export const Ok = (value: Ok) => ({ ok: true, value } as const); 6 | export const Err = (error: Err) => ({ ok: false, error } as const); 7 | 8 | export const val = (r: Result) => r.ok ? r.value : r.error; 9 | export const EMPTY_ERR = Err(undefined); 10 | 11 | /** 12 | * Wrap an async operation that may throw an Error (`try-catch` style) into checked exception style 13 | * @param op The operation function 14 | */ 15 | export async function wrapAsync(op: () => Promise): Promise> { 16 | try { return op() 17 | .then(Ok) 18 | .catch(Err); } 19 | catch (e) { return Promise.resolve(Err(e as E)); } 20 | } 21 | -------------------------------------------------------------------------------- /src/handlers/event-utils.ts: -------------------------------------------------------------------------------- 1 | import type { Emitter, Logging } from '../core/interfaces'; 2 | import { SernError } from '../core/structures/enums' 3 | import { Ok, wrapAsync} from '../core/structures/result'; 4 | import type { Module } from '../types/core-modules'; 5 | import { inspect } from 'node:util' 6 | import { resultPayload } from '../core/functions' 7 | import merge from 'deepmerge' 8 | 9 | 10 | interface ExecutePayload { 11 | module: Module; 12 | args: unknown[]; 13 | [key: string]: unknown 14 | } 15 | 16 | 17 | function isObject(item: unknown) { 18 | return (item && typeof item === 'object' && !Array.isArray(item)); 19 | } 20 | 21 | //_module is frozen, preventing from mutations 22 | export async function callInitPlugins(_module: Module, deps: Dependencies, emit?: boolean) { 23 | let module = _module; 24 | const emitter = deps['@sern/emitter']; 25 | for(const plugin of module.plugins ?? []) { 26 | const result = await plugin.execute({ module, absPath: module.meta.absPath, deps }); 27 | if (!result) throw Error("Plugin did not return anything. " + inspect(plugin, false, Infinity, true)); 28 | if(!result.ok) { 29 | if(emit) { 30 | emitter?.emit('module.register', 31 | resultPayload('failure', module, result.error ?? SernError.PluginFailure)); 32 | } 33 | throw Error((result.error ?? SernError.PluginFailure) + 34 | 'on module ' + module.name + " " + module.meta.absPath); 35 | } 36 | } 37 | return module 38 | } 39 | 40 | export function executeModule(emitter: Emitter, logger: Logging|undefined, { module, args } : ExecutePayload) { 41 | 42 | const moduleCalled = wrapAsync(async () => { 43 | return module.execute(...args); 44 | }) 45 | moduleCalled 46 | .then((res) => { 47 | if(res.ok) { 48 | emitter.emit('module.activate', resultPayload('success', module)) 49 | } else { 50 | if(!emitter.emit('error', resultPayload('failure', module, res.error))) { 51 | // node crashes here. 52 | logger?.error({ 'message': res.error }) 53 | } 54 | } 55 | }) 56 | .catch(err => { 57 | throw err 58 | }) 59 | }; 60 | 61 | 62 | export async function callPlugins({ args, module }: ExecutePayload) { 63 | let state = {}; 64 | for(const plugin of module.onEvent??[]) { 65 | const result = await plugin.execute(...args); 66 | if(!result.ok) { 67 | return result; 68 | } 69 | if(isObject(result.value)) { 70 | state = merge(state, result.value!); 71 | } 72 | } 73 | return Ok(state); 74 | } 75 | -------------------------------------------------------------------------------- /src/handlers/interaction.ts: -------------------------------------------------------------------------------- 1 | import type { Module, SernAutocompleteData } from '../types/core-modules' 2 | import { callPlugins, executeModule } from './event-utils'; 3 | import { SernError } from '../core/structures/enums' 4 | import { createSDT, isAutocomplete, isCommand, isContextCommand, isMessageComponent, isModal, resultPayload } from '../core/functions' 5 | import type { UnpackedDependencies } from '../types/utility'; 6 | import * as Id from '../core/id' 7 | import { Context } from '../core/structures/context'; 8 | import path from 'node:path'; 9 | 10 | 11 | 12 | export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: string) { 13 | //i wish javascript had clojure destructuring 14 | const { '@sern/client': client, 15 | '@sern/modules': moduleManager, 16 | '@sern/logger': log, 17 | '@sern/emitter': reporter } = deps 18 | 19 | client.on('interactionCreate', async (event) => { 20 | 21 | //returns array of possible ids 22 | const possibleIds = Id.reconstruct(event); 23 | 24 | let modules = possibleIds 25 | .map(({ id, params }) => ({ module: moduleManager.get(id)!, params })) 26 | .filter(({ module }) => module !== undefined); 27 | 28 | if(modules.length == 0) { 29 | return; 30 | } 31 | const { module, params } = modules.at(0)!; 32 | let payload; 33 | // handles autocomplete 34 | if(isAutocomplete(event)) { 35 | const lookupTable = module.locals['@sern/lookup-table'] as Map 36 | const subCommandGroup = event.options.getSubcommandGroup(false) ?? "", 37 | subCommand = event.options.getSubcommand(false) ?? "", 38 | option = event.options.getFocused(true), 39 | fullPath = path.posix.join("", subCommandGroup, subCommand, option.name) 40 | 41 | const resolvedModule = (lookupTable.get(fullPath)!.command) as Module 42 | payload= { module: resolvedModule , //autocomplete is not a true "module" warning cast! 43 | args: [event, createSDT(module, deps, params)] }; 44 | // either CommandTypes Slash | ContextMessage | ContextUesr 45 | } else if(isCommand(event)) { 46 | const sdt = createSDT(module, deps, params) 47 | // handle CommandType.CtxUser || CommandType.CtxMsg 48 | if(isContextCommand(event)) { 49 | payload= { module, args: [event, sdt] }; 50 | } else { 51 | // handle CommandType.Slash || CommandType.Both 52 | payload= { module, args: [Context.wrap(event, defaultPrefix), sdt] }; 53 | } 54 | // handles modals or components 55 | } else if (isModal(event) || isMessageComponent(event)) { 56 | payload= { module, args: [event, createSDT(module, deps, params)] } 57 | } else { 58 | throw Error("Unknown interaction while handling in interactionCreate event " + event) 59 | } 60 | const result = await callPlugins(payload) 61 | if(!result.ok) { 62 | reporter.emit('module.activate', resultPayload('failure', module, result.error ?? SernError.PluginFailure)) 63 | return 64 | } 65 | if(payload.args.length !== 2) { 66 | throw Error ('Invalid payload') 67 | } 68 | //@ts-ignore assigning final state from plugin 69 | payload.args[1].state = result.value 70 | 71 | // note: do not await this. will be blocking if long task (ie waiting for modal input) 72 | executeModule(reporter, log, payload); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /src/handlers/message.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from 'discord.js'; 2 | import { callPlugins, executeModule } from './event-utils'; 3 | import { SernError } from '../core/structures/enums' 4 | import { createSDT, fmt, resultPayload } from '../core/functions' 5 | import type { UnpackedDependencies } from '../types/utility'; 6 | import type { Module } from '../types/core-modules'; 7 | import { Context } from '../core/structures/context'; 8 | 9 | /** 10 | * Ignores messages from any person / bot except itself 11 | * @param prefix 12 | */ 13 | function isBotOrNoPrefix(msg: Message, prefix: string) { 14 | return msg.author.bot || !hasPrefix(prefix, msg.content); 15 | } 16 | 17 | function hasPrefix(prefix: string, content: string) { 18 | const prefixInContent = content.slice(0, prefix.length); 19 | return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0; 20 | } 21 | 22 | export function messageHandler (deps: UnpackedDependencies, defaultPrefix?: string) { 23 | const {"@sern/emitter": emitter, 24 | '@sern/logger': log, 25 | '@sern/modules': mg, 26 | '@sern/client': client} = deps 27 | 28 | if (!defaultPrefix) { 29 | log?.debug({ message: 'No prefix found. message handler shutting down' }); 30 | return; 31 | } 32 | client.on('messageCreate', async message => { 33 | if(isBotOrNoPrefix(message, defaultPrefix)) { 34 | return 35 | } 36 | const [prefix] = fmt(message.content, defaultPrefix); 37 | let module = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module; 38 | if(!module) { 39 | throw Error('Possibly undefined behavior: could not find a static id to resolve') 40 | } 41 | const payload = { module, args: [Context.wrap(message, defaultPrefix), createSDT(module, deps, undefined)] } 42 | const result = await callPlugins(payload) 43 | if (!result.ok) { 44 | emitter.emit('module.activate', resultPayload('failure', module, result.error ?? SernError.PluginFailure)) 45 | return 46 | } 47 | 48 | //@ts-ignore 49 | payload.args[1].state = result.value 50 | 51 | executeModule(emitter, log, payload) 52 | }) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/handlers/presence.ts: -------------------------------------------------------------------------------- 1 | import { Presence } from "../core/presences"; 2 | import { Services } from "../core/ioc"; 3 | import * as Files from "../core/module-loading"; 4 | type SetPresence = (conf: Presence.Result) => Promise 5 | 6 | const parseConfig = async (conf: Promise, setPresence: SetPresence) => { 7 | const result = await conf; 8 | 9 | if ('repeat' in result) { 10 | const { onRepeat, repeat } = result; 11 | // Validate configuration 12 | if (repeat === undefined) { 13 | throw new Error("repeat option is undefined"); 14 | } 15 | if (onRepeat === undefined) { 16 | throw new Error("onRepeat callback is undefined, but repeat exists"); 17 | } 18 | // Initial state 19 | let currentState = result; 20 | const processState = async (state: typeof currentState) => { 21 | try { 22 | const result = onRepeat(state); 23 | // If it's a promise, await it, otherwise use the value directly 24 | return result instanceof Promise ? await result : result; 25 | } catch (error) { 26 | // TODO process error 27 | //console.error(error); 28 | return state; // Return previous state on error 29 | } 30 | }; 31 | // Handle numeric interval 32 | if (typeof repeat === 'number') { 33 | // Return a promise that never resolves (or resolves on cleanup) 34 | return new Promise((resolve) => { 35 | // Immediately return initial state 36 | processState(currentState); 37 | 38 | // Set up interval 39 | let isProcessing = false; 40 | const intervalId = setInterval(() => { 41 | // Skip if previous operation is still running 42 | if (isProcessing) return; 43 | isProcessing = true; 44 | 45 | processState(currentState) 46 | .then(newState => { 47 | currentState = newState; 48 | return setPresence(currentState) 49 | }) 50 | .catch(console.error) 51 | .finally(() => { 52 | isProcessing = false; 53 | }); 54 | }, repeat); 55 | 56 | // Optional: Return cleanup function 57 | return () => clearInterval(intervalId); 58 | }); 59 | } 60 | // Handle event-based repeat 61 | else { 62 | const handler = async () => { 63 | currentState = await onRepeat(currentState); 64 | await setPresence(currentState); 65 | }; 66 | let has_registered = false; 67 | return new Promise((resolve) => { 68 | const [target, eventName] = repeat; 69 | 70 | // Immediately return initial state 71 | processState(currentState); 72 | 73 | // Set up event listener 74 | if(!has_registered) { 75 | target.addListener(eventName, handler); 76 | has_registered=true; 77 | } 78 | // Optional: Return cleanup function 79 | return () => target.removeListener(eventName, handler); 80 | }); 81 | } 82 | } 83 | 84 | // No repeat configuration, just return the result 85 | return setPresence(result); 86 | }; 87 | 88 | export const presenceHandler = async (path: string, setPresence: SetPresence) => { 89 | const presence = await 90 | Files.importModule>(path) 91 | .then(({ module }) => { 92 | //fetch services with the order preserved, passing it to the execute fn 93 | const fetchedServices = Services(...module.inject ?? []); 94 | return async () => module.execute(...fetchedServices); 95 | }) 96 | 97 | return parseConfig(presence(), setPresence); 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/handlers/ready.ts: -------------------------------------------------------------------------------- 1 | import * as Files from '../core/module-loading' 2 | import { once } from 'node:events'; 3 | import { createLookupTable, resultPayload } from '../core/functions'; 4 | import { CommandType } from '../core/structures/enums'; 5 | import { Module, SernOptionsData } from '../types/core-modules'; 6 | import type { UnpackedDependencies, Wrapper } from '../types/utility'; 7 | import { callInitPlugins } from './event-utils'; 8 | import { SernAutocompleteData } from '..'; 9 | 10 | export default async function(dirs: string | string[], deps : UnpackedDependencies) { 11 | const { '@sern/client': client, 12 | '@sern/logger': log, 13 | '@sern/emitter': sEmitter, 14 | '@sern/modules': commands } = deps; 15 | log?.info({ message: "Waiting on discord client to be ready..." }) 16 | await once(client, "ready"); 17 | log?.info({ message: "Client signaled ready, registering modules" }); 18 | 19 | // https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator 20 | // possibly optimize to concurrently import modules 21 | 22 | const directories = Array.isArray(dirs) ? dirs : [dirs]; 23 | 24 | for (const dir of directories) { 25 | for await (const path of Files.readRecursive(dir)) { 26 | let { module } = await Files.importModule(path); 27 | const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect; 28 | if(!validType) { 29 | throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``); 30 | } 31 | const resultModule = await callInitPlugins(module, deps, true); 32 | 33 | if(module.type === CommandType.Both || module.type === CommandType.Slash) { 34 | const options = (Reflect.get(module, 'options') ?? []) as SernOptionsData[]; 35 | const lookupTable = createLookupTable(options) 36 | module.locals['@sern/lookup-table'] = lookupTable; 37 | } 38 | // FREEZE! no more writing!! 39 | commands.set(resultModule.meta.id, Object.freeze(resultModule)); 40 | sEmitter.emit('module.register', resultPayload('success', resultModule)); 41 | } 42 | } 43 | sEmitter.emit('modulesLoaded'); 44 | } 45 | -------------------------------------------------------------------------------- /src/handlers/tasks.ts: -------------------------------------------------------------------------------- 1 | import * as Files from '../core/module-loading' 2 | import { UnpackedDependencies, Wrapper } from "../types/utility"; 3 | import type { ScheduledTask } from "../types/core-modules"; 4 | import { relative } from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | export const registerTasks = async (tasksDirs: string | string[], deps: UnpackedDependencies) => { 8 | const taskManager = deps['@sern/scheduler'] 9 | 10 | const directories = Array.isArray(tasksDirs) ? tasksDirs : [tasksDirs]; 11 | 12 | for (const dir of directories) { 13 | for await (const path of Files.readRecursive(dir)) { 14 | let { module } = await Files.importModule(path); 15 | //module.name is assigned by Files.importModule<> 16 | // the id created for the task is unique 17 | const uuid = module.name+"/"+relative(dir,fileURLToPath(path)) 18 | taskManager.schedule(uuid, module, deps) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/handlers/user-defined-events.ts: -------------------------------------------------------------------------------- 1 | import { EventType, SernError } from '../core/structures/enums'; 2 | import { callInitPlugins } from './event-utils' 3 | import { EventModule } from '../types/core-modules'; 4 | import * as Files from '../core/module-loading' 5 | import type { UnpackedDependencies } from '../types/utility'; 6 | import type { Emitter } from '../core/interfaces'; 7 | import { inspect } from 'util' 8 | import { resultPayload } from '../core/functions'; 9 | import type { Wrapper } from '../' 10 | 11 | export default async function(deps: UnpackedDependencies, wrapper: Wrapper) { 12 | const eventModules: EventModule[] = []; 13 | const eventDirs = Array.isArray(wrapper.events!) ? wrapper.events! : [wrapper.events!]; 14 | 15 | for (const dir of eventDirs) { 16 | for await (const path of Files.readRecursive(dir)) { 17 | let { module } = await Files.importModule(path); 18 | await callInitPlugins(module, deps) 19 | eventModules.push(module); 20 | } 21 | } 22 | 23 | const logger = deps['@sern/logger'], report = deps['@sern/emitter']; 24 | for (const module of eventModules) { 25 | let source: Emitter; 26 | 27 | switch (module.type) { 28 | case EventType.Sern: 29 | source=deps['@sern/emitter']; 30 | break 31 | case EventType.Discord: 32 | source=deps['@sern/client']; 33 | break 34 | case EventType.External: 35 | source=deps[module.emitter] as Emitter; 36 | break 37 | default: throw Error(SernError.InvalidModuleType + ' while creating event handler'); 38 | } 39 | if(!source && typeof source !== 'object') { 40 | throw Error(`${source} cannot be constructed into an event listener`) 41 | } 42 | 43 | if(!('addListener' in source && 'removeListener' in source)) { 44 | throw Error('source must implement Emitter') 45 | } 46 | const execute = async (...args: any[]) => { 47 | try { 48 | if(args) { 49 | if('once' in module) { source.removeListener(String(module.name!), execute); } 50 | await Reflect.apply(module.execute, null, args); 51 | } 52 | } catch(e) { 53 | const err = e instanceof Error ? e : Error(inspect(e, { colors: true })); 54 | if(!report.emit('error', resultPayload('failure', module, err))) { 55 | logger?.error({ message: inspect(err) }); 56 | } 57 | } 58 | } 59 | source.addListener(String(module.name!), execute) 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * as Sern from './sern'; 2 | 3 | export type { 4 | Module, 5 | CommandModule, 6 | EventModule, 7 | BothCommand, 8 | ContextMenuMsg, 9 | ContextMenuUser, 10 | SlashCommand, 11 | TextCommand, 12 | ButtonCommand, 13 | StringSelectCommand, 14 | MentionableSelectCommand, 15 | UserSelectCommand, 16 | ChannelSelectCommand, 17 | RoleSelectCommand, 18 | ModalSubmitCommand, 19 | DiscordEventCommand, 20 | SernEventCommand, 21 | ExternalEventCommand, 22 | CommandModuleDefs, 23 | EventModuleDefs, 24 | SernAutocompleteData, 25 | SernOptionsData, 26 | SernSubCommandData, 27 | SernSubCommandGroupData, 28 | SDT, 29 | ScheduledTask 30 | } from './types/core-modules'; 31 | 32 | export type { 33 | PluginResult, 34 | InitPlugin, 35 | ControlPlugin, 36 | Plugin, 37 | AnyPlugin, 38 | } from './types/core-plugin'; 39 | 40 | 41 | export type { Payload, SernEventsMapping, Wrapper } from './types/utility'; 42 | 43 | export { 44 | commandModule, 45 | eventModule, 46 | discordEvent, 47 | scheduledTask 48 | } from './core/modules'; 49 | 50 | export * from './core/presences' 51 | export * from './core/interfaces' 52 | export * from './core/plugin'; 53 | export { CommandType, PluginType, PayloadType, EventType } from './core/structures/enums'; 54 | export { Context } from './core/structures/context'; 55 | export { type CoreDependencies, makeDependencies, single, transient, Service, Services } from './core/ioc'; 56 | -------------------------------------------------------------------------------- /src/sern.ts: -------------------------------------------------------------------------------- 1 | //side effect: global container 2 | import { useContainerRaw } from '@sern/ioc/global'; 3 | // set asynchronous capturing of errors 4 | import events from 'node:events' 5 | events.captureRejections = true; 6 | 7 | import callsites from 'callsites'; 8 | import * as Files from './core/module-loading'; 9 | import eventsHandler from './handlers/user-defined-events'; 10 | import ready from './handlers/ready'; 11 | import { interactionHandler } from './handlers/interaction'; 12 | import { messageHandler } from './handlers/message' 13 | import { presenceHandler } from './handlers/presence'; 14 | import type { Payload, UnpackedDependencies, Wrapper } from './types/utility'; 15 | import type { Presence} from './core/presences'; 16 | import { registerTasks } from './handlers/tasks'; 17 | 18 | 19 | /** 20 | * @since 1.0.0 21 | * @param maybeWrapper Options to pass into sern. 22 | * Function to start the handler up 23 | * @example 24 | * ```ts title="src/index.ts" 25 | * Sern.init({ 26 | * commands: 'dist/commands', 27 | * events: 'dist/events', 28 | * }) 29 | * ``` 30 | */ 31 | 32 | export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { 33 | const startTime = performance.now(); 34 | const deps = useContainerRaw().deps(); 35 | if (maybeWrapper.events !== undefined) { 36 | eventsHandler(deps, maybeWrapper) 37 | .then(() => { 38 | deps['@sern/logger']?.info({ message: "Events registered" }); 39 | }); 40 | } else { 41 | deps['@sern/logger']?.info({ message: "No events registered" }); 42 | } 43 | 44 | // autohandle errors that occur in modules. 45 | // convenient for rapid iteration 46 | if(maybeWrapper.handleModuleErrors) { 47 | if(!deps['@sern/logger']) { 48 | throw Error('A logger is required to handleModuleErrors.\n A default logger is already supplied!'); 49 | } 50 | deps['@sern/logger']?.info({ 'message': 'handleModuleErrors enabled' }) 51 | deps['@sern/emitter'].addListener('error', (payload: Payload) => { 52 | if(payload.type === 'failure') { 53 | deps['@sern/logger']?.error({ message: payload.reason }) 54 | } else { 55 | deps['@sern/logger']?.warning({ message: "error event should only have payloads of 'failure'" }); 56 | } 57 | }) 58 | } 59 | 60 | const initCallsite = callsites()[1].getFileName(); 61 | const presencePath = Files.shouldHandle(initCallsite!, "presence"); 62 | //Ready event: load all modules and when finished, time should be taken and logged 63 | ready(maybeWrapper.commands, deps) 64 | .then(() => { 65 | const time = ((performance.now() - startTime) / 1000).toFixed(2); 66 | deps['@sern/logger']?.info({ message: `sern: registered in ${time} s` }); 67 | if(presencePath.exists) { 68 | const setPresence = async (p: Presence.Result) => { 69 | return deps['@sern/client'].user?.setPresence(p); 70 | } 71 | presenceHandler(presencePath.path, setPresence); 72 | } 73 | if(maybeWrapper.tasks) { 74 | registerTasks(maybeWrapper.tasks, deps); 75 | } 76 | }) 77 | .catch(err => { throw err }); 78 | interactionHandler(deps, maybeWrapper.defaultPrefix); 79 | messageHandler(deps, maybeWrapper.defaultPrefix) 80 | } 81 | -------------------------------------------------------------------------------- /src/types/core-modules.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIApplicationCommandBasicOption, 3 | APIApplicationCommandOptionBase, 4 | ApplicationCommandOptionType, 5 | BaseApplicationCommandOptionsData, 6 | AutocompleteInteraction, 7 | ButtonInteraction, 8 | ChannelSelectMenuInteraction, 9 | ClientEvents, 10 | MentionableSelectMenuInteraction, 11 | MessageContextMenuCommandInteraction, 12 | ModalSubmitInteraction, 13 | RoleSelectMenuInteraction, 14 | StringSelectMenuInteraction, 15 | UserContextMenuCommandInteraction, 16 | UserSelectMenuInteraction, 17 | ChatInputCommandInteraction, 18 | } from 'discord.js'; 19 | import type { CommandType, EventType } from '../core/structures/enums'; 20 | import { Context } from '../core/structures/context' 21 | import { ControlPlugin, InitPlugin, Plugin } from './core-plugin'; 22 | import { Awaitable, SernEventsMapping, UnpackedDependencies, Dictionary } from './utility'; 23 | 24 | /** 25 | * SDT (State, Dependencies, Type) interface represents the core data structure 26 | * passed through the plugin pipeline to command modules. 27 | * 28 | * @interface SDT 29 | * @template TState - Type parameter for the state object's structure 30 | * @template TDeps - Type parameter for dependencies interface 31 | * 32 | * @property {Record} state - Accumulated state data passed between plugins 33 | * @property {TDeps} deps - Instance of application dependencies 34 | * @property {CommandType} type - Command type identifier 35 | * @property {string} [params] - Optional parameters passed to the command 36 | * 37 | * @example 38 | * // Example of a plugin using SDT 39 | * const loggingPlugin = CommandControlPlugin((ctx, sdt: SDT) => { 40 | * console.log(`User ${ctx.user.id} executed command`); 41 | * return controller.next({ 'logging/timestamp': Date.now() }); 42 | * }); 43 | * 44 | * @example 45 | * // Example of state accumulation through multiple plugins 46 | * const plugin1 = CommandControlPlugin((ctx, sdt: SDT) => { 47 | * return controller.next({ 'plugin1/data': 'value1' }); 48 | * }); 49 | * 50 | * const plugin2 = CommandControlPlugin((ctx, sdt: SDT) => { 51 | * // Access previous state 52 | * const prevData = sdt.state['plugin1/data']; 53 | * return controller.next({ 'plugin2/data': 'value2' }); 54 | * }); 55 | * 56 | * @remarks 57 | * - State is immutable and accumulated through the plugin chain 58 | * - Keys in state should be namespaced to avoid collisions 59 | * - Dependencies are injected and available throughout the pipeline 60 | * - Type information helps plugins make type-safe decisions 61 | * 62 | * @see {@link CommandControlPlugin} for plugin implementation 63 | * @see {@link CommandType} for available command types 64 | * @see {@link Dependencies} for [dependency injection](https://sern.dev/v4/reference/dependencies/) interface 65 | */ 66 | export type SDT = { 67 | /** 68 | * Accumulated state passed between plugins in the pipeline. 69 | * Each plugin can add to or modify this state using controller.next(). 70 | * 71 | * @type {Record} 72 | * @example 73 | * // Good: Namespaced state key 74 | * { 'myPlugin/userData': { id: '123', name: 'User' } } 75 | * 76 | * // Avoid: Non-namespaced keys that might collide 77 | * { userData: { id: '123' } } 78 | */ 79 | state: Record; 80 | 81 | /** 82 | * Application dependencies available to plugins and command modules. 83 | * Typically includes services, configurations, and utilities. 84 | * 85 | * @type {Dependencies} 86 | */ 87 | deps: Dependencies; 88 | 89 | /** 90 | * Identifies the type of command being processed. 91 | * Used by plugins to apply type-specific logic. 92 | * 93 | * @type {CommandType} 94 | */ 95 | type: CommandType; 96 | 97 | /** 98 | * Optional parameters passed to the command. 99 | * May contain additional configuration or runtime data. 100 | * 101 | * @type {string} 102 | * @optional 103 | */ 104 | params?: string; 105 | 106 | /** 107 | * A copy of the current module that the plugin is running in. 108 | */ 109 | module: { name: string; 110 | description: string; 111 | meta: Dictionary; 112 | locals: Dictionary; } 113 | }; 114 | 115 | export type Processed = T & { name: string; description: string }; 116 | 117 | 118 | /** 119 | * @since 1.0.0 120 | */ 121 | export interface Module { 122 | type: CommandType | EventType; 123 | name?: string; 124 | onEvent: ControlPlugin[]; 125 | plugins: InitPlugin[]; 126 | description?: string; 127 | meta: { 128 | id: string; 129 | absPath: string; 130 | } 131 | 132 | /** 133 | * Custom data storage object for module-specific information. 134 | * Plugins and module code can use this to store and retrieve metadata, 135 | * configuration, or any other module-specific information. 136 | * 137 | * @type {Dictionary} 138 | * @description A key-value store that allows plugins and module code to persist 139 | * data at the module level. This is especially useful for InitPlugins that need 140 | * to attach metadata or configuration to modules. 141 | * 142 | * @example 143 | * // In a plugin 144 | * module.locals.registrationDate = Date.now(); 145 | * module.locals.version = "1.0.0"; 146 | * module.locals.permissions = ["ADMIN", "MODERATE"]; 147 | * 148 | * @example 149 | * // In module execution 150 | * console.log(`Command registered on: ${new Date(module.locals.registrationDate)}`); 151 | * 152 | * @example 153 | * // Storing localization data 154 | * module.locals.translations = { 155 | * en: "Hello", 156 | * es: "Hola", 157 | * fr: "Bonjour" 158 | * }; 159 | * 160 | * @example 161 | * // Storing command metadata 162 | * module.locals.metadata = { 163 | * category: "admin", 164 | * cooldown: 5000, 165 | * requiresPermissions: true 166 | * }; 167 | * 168 | * @remarks 169 | * - The locals object is initialized as an empty object ({}) by default 170 | * - Keys should be namespaced to avoid collisions between plugins 171 | * - Values can be of any type 172 | * - Data persists for the lifetime of the module 173 | * - Commonly used by InitPlugins during module initialization 174 | * 175 | * @best-practices 176 | * 1. Namespace your keys to avoid conflicts: 177 | * ```typescript 178 | * module.locals['myPlugin:data'] = value; 179 | * ``` 180 | * 181 | * 2. Document the data structure you're storing: 182 | * ```typescript 183 | * interface MyPluginData { 184 | * version: string; 185 | * timestamp: number; 186 | * } 187 | * module.locals['myPlugin:data'] = { 188 | * version: '1.0.0', 189 | * timestamp: Date.now() 190 | * } as MyPluginData; 191 | * ``` 192 | * 193 | * 3. Use type-safe accessors when possible: 194 | * ```typescript 195 | * const getPluginData = (module: Module): MyPluginData => 196 | * module.locals['myPlugin:data']; 197 | * ``` 198 | */ 199 | locals: Dictionary; 200 | execute(...args: any[]): Awaitable; 201 | } 202 | 203 | /** 204 | * @since 1.0.0 205 | */ 206 | export interface SernEventCommand 207 | extends Module { 208 | name?: T; 209 | type: EventType.Sern; 210 | execute(...args: SernEventsMapping[T]): Awaitable; 211 | } 212 | /** 213 | * @since 1.0.0 214 | */ 215 | export interface ExternalEventCommand extends Module { 216 | name?: string; 217 | emitter: keyof Dependencies; 218 | type: EventType.External; 219 | execute(...args: unknown[]): Awaitable; 220 | } 221 | 222 | /** 223 | * @since 1.0.0 224 | */ 225 | export interface ContextMenuUser extends Module { 226 | type: CommandType.CtxUser; 227 | execute: (ctx: UserContextMenuCommandInteraction, tbd: SDT) => Awaitable; 228 | } 229 | /** 230 | * @since 1.0.0 231 | */ 232 | export interface ContextMenuMsg extends Module { 233 | type: CommandType.CtxMsg; 234 | execute: (ctx: MessageContextMenuCommandInteraction, tbd: SDT) => Awaitable; 235 | } 236 | /** 237 | * @since 1.0.0 238 | */ 239 | export interface ButtonCommand extends Module { 240 | type: CommandType.Button; 241 | execute: (ctx: ButtonInteraction, tbd: SDT) => Awaitable; 242 | } 243 | /** 244 | * @since 1.0.0 245 | */ 246 | export interface StringSelectCommand extends Module { 247 | type: CommandType.StringSelect; 248 | execute: (ctx: StringSelectMenuInteraction, tbd: SDT) => Awaitable; 249 | } 250 | /** 251 | * @since 1.0.0 252 | */ 253 | export interface ChannelSelectCommand extends Module { 254 | type: CommandType.ChannelSelect; 255 | execute: (ctx: ChannelSelectMenuInteraction, tbd: SDT) => Awaitable; 256 | } 257 | /** 258 | * @since 1.0.0 259 | */ 260 | export interface RoleSelectCommand extends Module { 261 | type: CommandType.RoleSelect; 262 | execute: (ctx: RoleSelectMenuInteraction, tbd: SDT) => Awaitable; 263 | } 264 | /** 265 | * @since 1.0.0 266 | */ 267 | export interface MentionableSelectCommand extends Module { 268 | type: CommandType.MentionableSelect; 269 | execute: (ctx: MentionableSelectMenuInteraction, tbd: SDT) => Awaitable; 270 | } 271 | /** 272 | * @since 1.0.0 273 | */ 274 | export interface UserSelectCommand extends Module { 275 | type: CommandType.UserSelect; 276 | execute: (ctx: UserSelectMenuInteraction, tbd: SDT) => Awaitable; 277 | } 278 | /** 279 | * @since 1.0.0 280 | */ 281 | export interface ModalSubmitCommand extends Module { 282 | type: CommandType.Modal; 283 | execute: (ctx: ModalSubmitInteraction, tbd: SDT) => Awaitable; 284 | } 285 | /** 286 | * @since 1.0.0 287 | */ 288 | export interface AutocompleteCommand { 289 | onEvent?: ControlPlugin[]; 290 | execute: (ctx: AutocompleteInteraction, tbd: SDT) => Awaitable; 291 | } 292 | /** 293 | * @since 1.0.0 294 | */ 295 | export interface DiscordEventCommand 296 | extends Module { 297 | name?: T; 298 | type: EventType.Discord; 299 | execute(...args: ClientEvents[T]): Awaitable; 300 | } 301 | /** 302 | * @since 1.0.0 303 | * @see @link {commandModule} to create a text command 304 | */ 305 | export interface TextCommand extends Module { 306 | type: CommandType.Text; 307 | execute: (ctx: Context & { get options(): string[] }, tbd: SDT) => Awaitable; 308 | } 309 | /** 310 | * @since 1.0.0 311 | * @see @link {commandModule} to create a slash command 312 | */ 313 | export interface SlashCommand extends Module { 314 | type: CommandType.Slash; 315 | description: string; 316 | options?: SernOptionsData[]; 317 | execute: (ctx: Context & { get options(): ChatInputCommandInteraction['options']}, tbd: SDT) => Awaitable; 318 | } 319 | /** 320 | * @since 1.0.0 321 | * @see @link {commandModule} to create a both command 322 | */ 323 | export interface BothCommand extends Module { 324 | type: CommandType.Both; 325 | description: string; 326 | options?: SernOptionsData[]; 327 | execute: (ctx: Context, tbd: SDT) => Awaitable; 328 | } 329 | /** 330 | * @since 1.0.0 331 | */ 332 | export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand; 333 | 334 | /** 335 | * @since 1.0.0 336 | */ 337 | export type CommandModule = 338 | | TextCommand 339 | | SlashCommand 340 | | BothCommand 341 | | ContextMenuUser 342 | | ContextMenuMsg 343 | | ButtonCommand 344 | | StringSelectCommand 345 | | MentionableSelectCommand 346 | | UserSelectCommand 347 | | ChannelSelectCommand 348 | | RoleSelectCommand 349 | | ModalSubmitCommand; 350 | 351 | //https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union 352 | // Explicit Module Definitions for mapping 353 | export interface CommandModuleDefs { 354 | [CommandType.Text]: TextCommand; 355 | [CommandType.Slash]: SlashCommand; 356 | [CommandType.Both]: BothCommand; 357 | [CommandType.CtxMsg]: ContextMenuMsg; 358 | [CommandType.CtxUser]: ContextMenuUser; 359 | [CommandType.Button]: ButtonCommand; 360 | [CommandType.StringSelect]: StringSelectCommand; 361 | [CommandType.RoleSelect]: RoleSelectCommand; 362 | [CommandType.ChannelSelect]: ChannelSelectCommand; 363 | [CommandType.MentionableSelect]: MentionableSelectCommand; 364 | [CommandType.UserSelect]: UserSelectCommand; 365 | [CommandType.Modal]: ModalSubmitCommand; 366 | } 367 | 368 | export interface EventModuleDefs { 369 | [EventType.Sern]: SernEventCommand; 370 | [EventType.Discord]: DiscordEventCommand; 371 | [EventType.External]: ExternalEventCommand; 372 | } 373 | 374 | export interface SernAutocompleteData 375 | extends Omit { 376 | autocomplete: true; 377 | type: 378 | | ApplicationCommandOptionType.String 379 | | ApplicationCommandOptionType.Number 380 | | ApplicationCommandOptionType.Integer; 381 | command: AutocompleteCommand; 382 | } 383 | 384 | type CommandModuleNoPlugins = { 385 | [T in CommandType]: Omit; 386 | }; 387 | type EventModulesNoPlugins = { 388 | [T in EventType]: Omit[T], 'plugins' | 'onEvent' | 'meta' | 'locals'> ; 389 | }; 390 | 391 | export type InputEvent = { 392 | [T in EventType]: EventModulesNoPlugins[T] & { 393 | once?: boolean; 394 | plugins?: InitPlugin[] 395 | }; 396 | }[EventType]; 397 | 398 | export type InputCommand = { 399 | [T in CommandType]: CommandModuleNoPlugins[T] & { 400 | plugins?: Plugin[]; 401 | }; 402 | }[CommandType]; 403 | 404 | /** 405 | * @see @link {https://sern.dev/v4/reference/autocomplete/} 406 | * Type that replaces autocomplete with {@link SernAutocompleteData} 407 | */ 408 | export type SernOptionsData = 409 | | SernSubCommandData 410 | | SernSubCommandGroupData 411 | | APIApplicationCommandBasicOption 412 | | SernAutocompleteData; 413 | 414 | export interface SernSubCommandData 415 | extends APIApplicationCommandOptionBase { 416 | type: ApplicationCommandOptionType.Subcommand; 417 | options?: SernOptionsData[]; 418 | } 419 | 420 | export interface SernSubCommandGroupData extends BaseApplicationCommandOptionsData { 421 | type: ApplicationCommandOptionType.SubcommandGroup; 422 | options?: SernSubCommandData[]; 423 | } 424 | 425 | /** 426 | * @since 4.0.0 427 | */ 428 | export interface ScheduledTaskContext { 429 | 430 | /** 431 | * the uuid of the current task being run 432 | */ 433 | id: string; 434 | /** 435 | * the last time this task was executed. If this is the first time, it is null. 436 | */ 437 | lastTimeExecution: Date | null; 438 | /** 439 | * The next time this task will be executed. 440 | */ 441 | nextTimeExecution: Date | null; 442 | } 443 | 444 | //name subject to change 445 | interface TaskAttrs { 446 | /** 447 | * An object of dependencies configured in `makeDependencies` 448 | */ 449 | deps: UnpackedDependencies 450 | } 451 | /** 452 | * @since 4.0.0 453 | */ 454 | export interface ScheduledTask { 455 | name?: string; 456 | trigger: string | Date; 457 | timezone?: string; 458 | execute(tasks: ScheduledTaskContext, sdt: TaskAttrs): Awaitable 459 | } 460 | 461 | 462 | -------------------------------------------------------------------------------- /src/types/core-plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Plugins can be inserted on all commands and are emitted 3 | * 4 | * 1. On ready event, where all commands are loaded. 5 | * 2. On corresponding observable (when command triggers) 6 | * 7 | * The goal of plugins is to organize commands and 8 | * provide extensions to repetitive patterns 9 | * examples include refreshing modules, 10 | * categorizing commands, cool-downs, permissions, etc. 11 | * Plugins are reminiscent of middleware in express. 12 | */ 13 | 14 | import type { 15 | Module, 16 | Processed, 17 | SDT, 18 | } from './core-modules'; 19 | import type { Awaitable } from './utility'; 20 | import type { CommandType, PluginType } from '../core/structures/enums' 21 | import type { Context } from '../core/structures/context' 22 | import type { 23 | ButtonInteraction, 24 | ChannelSelectMenuInteraction, 25 | ChatInputCommandInteraction, 26 | MentionableSelectMenuInteraction, 27 | MessageContextMenuCommandInteraction, 28 | ModalSubmitInteraction, 29 | RoleSelectMenuInteraction, 30 | StringSelectMenuInteraction, 31 | UserContextMenuCommandInteraction, 32 | UserSelectMenuInteraction, 33 | } from 'discord.js'; 34 | import { Result } from '../core/structures/result'; 35 | 36 | export type PluginResult = Awaitable|undefined, string|undefined>>; 37 | export interface InitArgs = Processed> { 38 | module: T; 39 | absPath: string; 40 | deps: Dependencies 41 | } 42 | export interface Plugin { 43 | type: PluginType; 44 | execute: (...args: Args) => PluginResult; 45 | } 46 | 47 | export interface InitPlugin extends Plugin { 48 | type: PluginType.Init; 49 | execute: (...args: Args) => PluginResult; 50 | } 51 | export interface ControlPlugin extends Plugin { 52 | type: PluginType.Control; 53 | } 54 | 55 | export type AnyPlugin = ControlPlugin | InitPlugin<[InitArgs>]>; 56 | 57 | export type CommandArgs = CommandArgsMatrix[I] 58 | 59 | interface CommandArgsMatrix { 60 | [CommandType.Text]: [Context & { get options(): string[]}, SDT]; 61 | [CommandType.Slash]: [Context & { get options(): ChatInputCommandInteraction['options']}, SDT]; 62 | [CommandType.Both]: [Context, SDT]; 63 | [CommandType.CtxMsg]: [MessageContextMenuCommandInteraction, SDT]; 64 | [CommandType.CtxUser]: [UserContextMenuCommandInteraction, SDT]; 65 | [CommandType.Button]: [ButtonInteraction, SDT]; 66 | [CommandType.StringSelect]: [StringSelectMenuInteraction, SDT]; 67 | [CommandType.RoleSelect]: [RoleSelectMenuInteraction, SDT]; 68 | [CommandType.ChannelSelect]: [ChannelSelectMenuInteraction, SDT]; 69 | [CommandType.MentionableSelect]: [MentionableSelectMenuInteraction, SDT]; 70 | [CommandType.UserSelect]: [UserSelectMenuInteraction, SDT]; 71 | [CommandType.Modal]: [ModalSubmitInteraction, SDT]; 72 | } 73 | -------------------------------------------------------------------------------- /src/types/dependencies.d.ts: -------------------------------------------------------------------------------- 1 | // This file serves an the interface for developers to augment the Dependencies interface 2 | // Developers will have to create a new file dependencies.d.ts in the root directory, augmenting 3 | // this type 4 | 5 | /* eslint-disable @typescript-eslint/consistent-type-imports */ 6 | 7 | import { CoreDependencies } from '../core/ioc'; 8 | 9 | declare global { 10 | /** 11 | * discord.js client. 12 | * '@sern/client': Client 13 | * sern emitter listens to events that happen throughout 14 | * the handler. some include module.register, module.activate. 15 | * '@sern/emitter': Contracts.Emitter; 16 | * An error handler which is the final step before 17 | * the sern process actually crashes. 18 | '@sern/errors': Contracts.ErrorHandling; 19 | * Optional logger. Performs ... logging 20 | * '@sern/logger'?: Contracts.Logging; 21 | * Readonly module store. sern stores these 22 | * by module.meta.id -> Module 23 | * '@sern/modules': Map; 24 | */ 25 | interface Dependencies extends CoreDependencies {} 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/types/utility.ts: -------------------------------------------------------------------------------- 1 | import type { InteractionReplyOptions, MessageReplyOptions } from 'discord.js'; 2 | import type { Module } from './core-modules'; 3 | 4 | export type Awaitable = PromiseLike | T; 5 | export type Dictionary = Record 6 | 7 | export type AnyFunction = (...args: any[]) => unknown; 8 | 9 | export interface SernEventsMapping { 10 | 'module.register': [Payload]; 11 | 'module.activate': [Payload]; 12 | error: [{ type: 'failure'; module?: Module; reason: string | Error }]; 13 | warning: [Payload]; 14 | modulesLoaded: [never?]; 15 | } 16 | 17 | export type Payload = 18 | | { type: 'success'; module: Module } 19 | | { type: 'failure'; module?: Module; reason: string | Error } 20 | | { type: 'warning'; module: undefined; reason: string }; 21 | 22 | export type UnpackFunction = T extends (...args: any) => infer U ? U : T 23 | export type UnpackedDependencies = { 24 | [K in keyof Dependencies]: UnpackFunction 25 | } 26 | export type ReplyOptions = string | Omit | MessageReplyOptions; 27 | 28 | 29 | /** 30 | * @interface Wrapper 31 | * @description Configuration interface for the sern framework. This interface defines 32 | * the structure for configuring essential framework features including command handling, 33 | * event management, and task scheduling. 34 | */ 35 | export interface Wrapper { 36 | /** 37 | * @property {string|string[]} commands 38 | * @description Specifies the directory path where command modules are located. 39 | * This is a required property that tells sern where to find and load command files. 40 | * The path should be relative to the project root. If given an array, each directory is loaded in order 41 | * they were declared. Order of modules in each directory is not guaranteed 42 | * 43 | * @example 44 | * commands: ["./dist/commands"] 45 | */ 46 | commands: string | string[]; 47 | /** 48 | * @property {boolean} [handleModuleErrors] 49 | * @description Optional flag to enable automatic error handling for modules. 50 | * When enabled, sern will automatically catch and handle errors that occur 51 | * during module execution, preventing crashes and providing error logging. 52 | * 53 | * @default false 54 | */ 55 | handleModuleErrors?: boolean; 56 | /** 57 | * @property {string} [defaultPrefix] 58 | * @description Optional prefix for text commands. This prefix will be used 59 | * to identify text commands in messages. If not specified, text commands {@link CommandType.Text} 60 | * will be disabled. 61 | * 62 | * @example 63 | * defaultPrefix: "?" 64 | */ 65 | defaultPrefix?: string; 66 | /** 67 | * @property {string|string[]} [events] 68 | * @description Optional directory path where event modules are located. 69 | * If provided, Sern will automatically register and handle events from 70 | * modules in this directory. The path should be relative to the project root. 71 | * If given an array, each directory is loaded in order they were declared. 72 | * Order of modules in each directory is not guaranteed. 73 | * 74 | * @example 75 | * events: ["./dist/events"] 76 | */ 77 | events?: string | string[]; 78 | /** 79 | * @property {string|string[]} [tasks] 80 | * @description Optional directory path where scheduled task modules are located. 81 | * If provided, Sern will automatically register and handle scheduled tasks 82 | * from modules in this directory. The path should be relative to the project root. 83 | * If given an array, each directory is loaded in order they were declared. 84 | * Order of modules in each directory is not guaranteed. 85 | * 86 | * @example 87 | * tasks: ["./dist/tasks"] 88 | */ 89 | tasks?: string | string[]; 90 | } 91 | -------------------------------------------------------------------------------- /test/autocomp.bench.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'node:test' 2 | import { bench } from 'vitest' 3 | import { SernAutocompleteData, SernOptionsData } from '../src' 4 | import { createRandomChoice } from './setup/util' 5 | import { ApplicationCommandOptionType, AutocompleteFocusedOption, AutocompleteInteraction } from 'discord.js' 6 | import { createLookupTable } from '../src/core/functions' 7 | import assert from 'node:assert' 8 | 9 | /** 10 | * Uses an iterative DFS to check if an autocomplete node exists on the option tree 11 | * This is the old internal method that sern used to resolve autocomplete 12 | * @param iAutocomplete 13 | * @param options 14 | */ 15 | function treeSearch( 16 | choice: AutocompleteFocusedOption, 17 | parent: string|undefined, 18 | options: SernOptionsData[] | undefined, 19 | ): SernAutocompleteData & { parent?: string } | undefined { 20 | if (options === undefined) return undefined; 21 | //clone to prevent mutation of original command module 22 | const _options = options.map(a => ({ ...a })); 23 | const subcommands = new Set(); 24 | while (_options.length > 0) { 25 | const cur = _options.pop()!; 26 | switch (cur.type) { 27 | case ApplicationCommandOptionType.Subcommand: { 28 | subcommands.add(cur.name); 29 | for (const option of cur.options ?? []) _options.push(option); 30 | } break; 31 | case ApplicationCommandOptionType.SubcommandGroup: { 32 | for (const command of cur.options ?? []) _options.push(command); 33 | } break; 34 | default: { 35 | if ('autocomplete' in cur && cur.autocomplete) { 36 | assert( 'command' in cur, 'No `command` property found for option ' + cur.name); 37 | if (subcommands.size > 0) { 38 | const parentAndOptionMatches = 39 | subcommands.has(parent) && cur.name === choice.name; 40 | if (parentAndOptionMatches) { 41 | return { ...cur, parent }; 42 | } 43 | } else { 44 | if (cur.name === choice.name) { 45 | return { ...cur, parent: undefined }; 46 | } 47 | } 48 | } 49 | } break; 50 | } 51 | } 52 | } 53 | 54 | const options: SernOptionsData[] = [ 55 | createRandomChoice(), 56 | createRandomChoice(), 57 | createRandomChoice(), 58 | { 59 | type: ApplicationCommandOptionType.String, 60 | name: 'autocomplete', 61 | description: 'here', 62 | autocomplete: true, 63 | command: { onEvent: [], execute: () => {} }, 64 | }, 65 | ] 66 | 67 | 68 | const table = createLookupTable(options) 69 | 70 | 71 | describe('autocomplete lookup', () => { 72 | 73 | bench('lookup table', () => { 74 | table.get('/autocomplete') 75 | }, { time: 500 }) 76 | 77 | 78 | bench('naive treeSearch', () => { 79 | treeSearch({ focused: true, 80 | name: 'autocomplete', 81 | value: 'autocomplete', 82 | type: ApplicationCommandOptionType.String }, undefined, options) 83 | }, { time: 500 }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/core/context.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, vi, it, expect } from'vitest' 2 | import { Context } from '../../src'; 3 | import { faker } from '@faker-js/faker' 4 | describe('Context', () => { 5 | // Mocked message and interaction objects for testing 6 | const mockMessage = { 7 | id: 'messageId', 8 | channel: 'channelId', 9 | channelId: 'channelId', 10 | interaction: { 11 | id: faker.string.uuid() 12 | }, 13 | author: { id: 'userId' }, 14 | createdTimestamp: 1234567890, 15 | guild: 'guildId', 16 | guildId: 'guildId', 17 | member: { id: 'memberId' }, 18 | client: { id: 'clientId' }, 19 | inGuild: vi.fn().mockReturnValue(true), 20 | reply: vi.fn(), 21 | }; 22 | 23 | const mockInteraction = { 24 | id: 'interactionId', 25 | user: { id: 'userId' }, 26 | channel: 'channelId', 27 | channelId: 'channelId', 28 | createdTimestamp: 1234567890, 29 | guild: 'guildId', 30 | guildId: 'guildId', 31 | fetchReply: vi.fn().mockResolvedValue({}), 32 | member: { id: 'memberId' }, 33 | client: { id: 'clientId' }, 34 | isChatInputCommand: vi.fn().mockResolvedValue(true), 35 | inGuild: vi.fn().mockReturnValue(true), 36 | reply: vi.fn().mockResolvedValue({}), 37 | }; 38 | 39 | it('should create a context from a message', () => { 40 | //@ts-ignore 41 | const context = Context.wrap(mockMessage); 42 | expect(context).toBeDefined(); 43 | expect(context.id).toBe('messageId'); 44 | }); 45 | it('should throw error if accessing interaction as message', () => { 46 | //@ts-ignore 47 | const context = Context.wrap(mockMessage); 48 | expect(context).toBeDefined(); 49 | expect(() => context.interaction) 50 | .toThrowError('You cannot use message when an interaction fired or vice versa'); 51 | 52 | }) 53 | it('should throw error if accessing message as interaction', () => { 54 | //@ts-ignore 55 | const context = Context.wrap(mockInteraction); 56 | expect(context).toBeDefined(); 57 | expect(() => context.message) 58 | .toThrowError('You cannot use message when an interaction fired or vice versa'); 59 | 60 | }) 61 | 62 | it('should create a context from an interaction', () => { 63 | //@ts-ignore 64 | const context = Context.wrap(mockInteraction); 65 | expect(context).toBeDefined(); 66 | expect(context.id).toBe('interactionId'); 67 | }); 68 | 69 | it('should reply to a context with a message', async () => { 70 | //@ts-ignore 71 | const context = Context.wrap(mockMessage); 72 | const replyOptions = { content: 'Hello, world!' }; 73 | await context.reply(replyOptions); 74 | expect(mockMessage.reply).toHaveBeenCalledWith(replyOptions); 75 | }); 76 | 77 | it('should reply to a context with an interaction', async () => { 78 | //@ts-ignore 79 | const context = Context.wrap(mockInteraction); 80 | const replyOptions = { content: 'Hello, world!' }; 81 | await context.reply(replyOptions); 82 | expect(mockInteraction.reply).toHaveBeenCalledWith(replyOptions); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /test/core/contracts.test.ts: -------------------------------------------------------------------------------- 1 | import { assertType, describe, it } from 'vitest'; 2 | 3 | import * as __Services from '../../src/core/structures/default-services'; 4 | import * as Contracts from '../../src/core/interfaces'; 5 | 6 | describe('default contracts', () => { 7 | it('should satisfy contracts', () => { 8 | assertType(new __Services.DefaultLogging()); 9 | assertType(new __Services.DefaultErrorHandling()); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/core/create-plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { 3 | CommandControlPlugin, 4 | CommandInitPlugin, 5 | EventInitPlugin, 6 | } from '../../src'; 7 | import { PluginType, controller } from '../../src'; 8 | 9 | describe('create-plugins', () => { 10 | it('should make proper control plugins', () => { 11 | const pl2 = CommandControlPlugin(() => controller.next()); 12 | expect(pl2).to.have.all.keys(['type', 'execute']); 13 | expect(pl2.type).toBe(PluginType.Control); 14 | expect(pl2.execute).an('function'); 15 | }); 16 | it('should make proper init plugins', () => { 17 | const pl = EventInitPlugin(() => controller.next()); 18 | expect(pl).to.have.all.keys(['type', 'execute']); 19 | expect(pl.type).toBe(PluginType.Init); 20 | expect(pl.execute).an('function'); 21 | 22 | const pl2 = CommandInitPlugin(() => controller.next()); 23 | expect(pl2).to.have.all.keys(['type', 'execute']); 24 | expect(pl2.type).toBe(PluginType.Init); 25 | expect(pl2.execute).an('function'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/core/functions.test.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { afterEach, describe, expect, it, vi } from 'vitest'; 3 | import { PluginType, SernOptionsData, controller } from '../../src/index'; 4 | import { createLookupTable, partitionPlugins, treeSearch } from '../../src/core/functions'; 5 | import { faker } from '@faker-js/faker'; 6 | import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js'; 7 | import { createRandomChoice, createRandomPlugins } from '../setup/util'; 8 | 9 | describe('functions', () => { 10 | afterEach(() => { 11 | vi.clearAllMocks(); 12 | }); 13 | 14 | 15 | it('should partition plugins correctly', () => { 16 | const plugins = createRandomPlugins(100); 17 | const [onEvent, init] = partitionPlugins(plugins); 18 | for (const el of onEvent) expect(el.type).to.equal(PluginType.Control); 19 | 20 | for (const el of init) expect(el.type).to.equal(PluginType.Init); 21 | }); 22 | 23 | describe('autocomplete', ( ) => { 24 | 25 | it('should tree search options tree depth 1', () => { 26 | const options: SernOptionsData[] = [ 27 | createRandomChoice(), 28 | { 29 | type: ApplicationCommandOptionType.String, 30 | name: 'autocomplete', 31 | description: 'here', 32 | autocomplete: true, 33 | command: { onEvent: [], execute: vi.fn() }, 34 | }, 35 | ]; 36 | const table = createLookupTable(options) 37 | const result = table.get('/autocomplete') 38 | expect(result == undefined).to.be.false; 39 | expect(result.name).to.be.eq('autocomplete'); 40 | expect(result.command).to.be.not.undefined; 41 | }), 42 | it('should tree search depth 2', () => { 43 | const subcommandName = faker.string.alpha(); 44 | const options: SernOptionsData[] = [ 45 | { 46 | type: ApplicationCommandOptionType.Subcommand, 47 | name: subcommandName, 48 | description: faker.string.alpha(), 49 | options: [ 50 | createRandomChoice(), 51 | createRandomChoice(), 52 | createRandomChoice(), 53 | { 54 | type: ApplicationCommandOptionType.String, 55 | name: 'nested', 56 | description: faker.string.alpha(), 57 | autocomplete: true, 58 | command: { 59 | onEvent: [], 60 | execute: () => {}, 61 | }, 62 | }, 63 | ], 64 | }, 65 | ]; 66 | const table = createLookupTable(options) 67 | const result = table.get(`/${subcommandName}/nested`) 68 | expect(result == undefined).to.be.false; 69 | expect(result.name).to.be.eq('nested'); 70 | expect(result.command).to.be.not.undefined; 71 | }); 72 | 73 | it('should tree search depth n > 2', () => { 74 | const subgroupName = faker.string.alpha() 75 | const subcommandName = faker.string.alpha(); 76 | const options: SernOptionsData[] = [ 77 | { 78 | type: ApplicationCommandOptionType.SubcommandGroup, 79 | name: subgroupName, 80 | description: faker.string.alpha(), 81 | options: [ 82 | { 83 | type: ApplicationCommandOptionType.Subcommand, 84 | name: subcommandName, 85 | description: faker.string.alpha(), 86 | options: [ 87 | createRandomChoice(), 88 | createRandomChoice(), 89 | { 90 | type: ApplicationCommandOptionType.String, 91 | name: 'nested', 92 | description: faker.string.alpha(), 93 | autocomplete: true, 94 | command: { 95 | onEvent: [], 96 | execute: () => {}, 97 | }, 98 | }, 99 | createRandomChoice(), 100 | ], 101 | }, 102 | ], 103 | }, 104 | ]; 105 | const table = createLookupTable(options) 106 | const result = table.get(`/${subgroupName}/${subcommandName}/nested`) 107 | expect(result == undefined).to.be.false; 108 | expect(result.name).to.be.eq('nested'); 109 | expect(result.command).to.be.not.undefined; 110 | }); 111 | 112 | it('should correctly resolve suboption of the same name given two subcommands ', () => { 113 | const subcommandName = faker.string.alpha(); 114 | const groupname = faker.string.alpha() 115 | const options: SernOptionsData[] = [ 116 | { 117 | type: ApplicationCommandOptionType.SubcommandGroup, 118 | name: groupname, 119 | description: faker.string.alpha(), 120 | options: [ 121 | { 122 | type: ApplicationCommandOptionType.Subcommand, 123 | name: subcommandName, 124 | description: faker.string.alpha(), 125 | options: [ 126 | createRandomChoice(), 127 | createRandomChoice(), 128 | { 129 | type: ApplicationCommandOptionType.String, 130 | name: 'nested', 131 | description: faker.string.alpha(), 132 | autocomplete: true, 133 | command: { 134 | onEvent: [], 135 | execute: () => {}, 136 | }, 137 | }, 138 | ], 139 | }, 140 | { 141 | type: ApplicationCommandOptionType.Subcommand, 142 | name: subcommandName + 'a', 143 | description: faker.string.alpha(), 144 | options: [ 145 | createRandomChoice(), 146 | { 147 | type: ApplicationCommandOptionType.String, 148 | name: 'nested', 149 | description: faker.string.alpha(), 150 | autocomplete: true, 151 | command: { 152 | onEvent: [], 153 | execute: () => {}, 154 | }, 155 | }, 156 | ], 157 | }, 158 | ], 159 | }, 160 | ]; 161 | const table = createLookupTable(options) 162 | const result = table.get(`/${groupname}/${subcommandName}/nested`); 163 | expect(result).toBeTruthy(); 164 | expect(result.name).to.be.eq('nested'); 165 | expect(result.command).to.be.not.undefined; 166 | }); 167 | it('two subcommands with an option of the same name', () => { 168 | const groupName = faker.string.alpha() 169 | const subcommandName = faker.string.alpha(); 170 | const options: SernOptionsData[] = [ 171 | { 172 | type: ApplicationCommandOptionType.SubcommandGroup, 173 | name: groupName, 174 | description: faker.string.alpha(), 175 | options: [ 176 | { 177 | type: ApplicationCommandOptionType.Subcommand, 178 | name: subcommandName, 179 | description: faker.string.alpha(), 180 | options: [ 181 | createRandomChoice(), 182 | createRandomChoice(), 183 | { 184 | type: ApplicationCommandOptionType.String, 185 | name: 'nested', 186 | description: faker.string.alpha(), 187 | autocomplete: true, 188 | command: { 189 | onEvent: [], 190 | execute: () => {}, 191 | }, 192 | }, 193 | ], 194 | }, 195 | { 196 | type: ApplicationCommandOptionType.Subcommand, 197 | name: subcommandName + 'anothera', 198 | description: faker.string.alpha(), 199 | options: [ 200 | createRandomChoice(), 201 | { 202 | type: ApplicationCommandOptionType.String, 203 | name: 'nested', 204 | description: faker.string.alpha(), 205 | autocomplete: true, 206 | command: { 207 | onEvent: [], 208 | execute: () => {}, 209 | }, 210 | }, 211 | ], 212 | }, 213 | ], 214 | }, 215 | ]; 216 | 217 | const table = createLookupTable(options) 218 | const result = table.get(`/${groupName}/${subcommandName}/nested`); 219 | expect(result).toBeTruthy(); 220 | expect(result.name).to.be.eq('nested'); 221 | expect(result.command).to.be.not.undefined; 222 | 223 | 224 | }); 225 | 226 | it('simulates autocomplete typing and resolution', () => { 227 | const subcommandGroupName = faker.string.alpha() 228 | const subcommandName = faker.string.alpha(); 229 | const optionName = faker.word.noun(); 230 | const options: SernOptionsData[] = [ 231 | { 232 | type: ApplicationCommandOptionType.SubcommandGroup, 233 | name: subcommandGroupName, 234 | description: faker.string.alpha(), 235 | options: [ 236 | { 237 | type: ApplicationCommandOptionType.Subcommand, 238 | name: subcommandName, 239 | description: faker.string.alpha(), 240 | options: [ 241 | createRandomChoice(), 242 | createRandomChoice(), 243 | { 244 | type: ApplicationCommandOptionType.String, 245 | name: optionName, 246 | description: faker.string.alpha(), 247 | autocomplete: true, 248 | command: { 249 | onEvent: [], 250 | execute: vi.fn(), 251 | }, 252 | }, 253 | ], 254 | }, 255 | { 256 | type: ApplicationCommandOptionType.Subcommand, 257 | name: subcommandName + 'a', 258 | description: faker.string.alpha(), 259 | options: [ 260 | createRandomChoice(), 261 | { 262 | type: ApplicationCommandOptionType.String, 263 | name: optionName, 264 | description: faker.string.alpha(), 265 | autocomplete: true, 266 | command: { 267 | onEvent: [], 268 | execute: vi.fn(), 269 | }, 270 | }, 271 | ], 272 | }, 273 | ], 274 | }, 275 | ]; 276 | let accumulator = ''; 277 | let result: unknown; 278 | const table = createLookupTable(options) 279 | for (const char of optionName) { 280 | accumulator += char; 281 | 282 | const focusedValue = { 283 | name: accumulator, 284 | value: faker.string.alpha(), 285 | focused: true, 286 | }; 287 | result = table.get(`/${subcommandGroupName}/${subcommandName}/${focusedValue.name}` ); 288 | } 289 | expect(result).toBeTruthy(); 290 | }); 291 | }) 292 | 293 | 294 | }); 295 | -------------------------------------------------------------------------------- /test/core/id.test.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { expect, test, vi } from 'vitest' 3 | import { CommandType } from '../../src/core/structures/enums'; 4 | 5 | import * as Id from '../../src/core/id' 6 | import { ButtonInteraction, ModalSubmitInteraction } from 'discord.js'; 7 | 8 | test('id -> Text', () => { 9 | expect(Id.create("ping", CommandType.Text)).toBe("ping_T") 10 | }) 11 | 12 | test('id -> Both', () => { 13 | expect(Id.create("ping", CommandType.Both)).toBe("ping_B") 14 | }) 15 | 16 | test('id -> CtxMsg', () => { 17 | expect(Id.create("ping", CommandType.CtxMsg)).toBe("ping_A3") 18 | }) 19 | test('id -> CtxUsr', () => { 20 | expect(Id.create("ping", CommandType.CtxUser)).toBe("ping_A2") 21 | }) 22 | test('id -> Modal', () => { 23 | expect(Id.create("my-modal", CommandType.Modal)).toBe("my-modal_M"); 24 | }) 25 | 26 | test('id -> Button', () => { 27 | expect(Id.create("my-button", CommandType.Button)).toBe("my-button_C2"); 28 | }) 29 | 30 | test('id -> Slash', () => { 31 | expect(Id.create("myslash", CommandType.Slash)).toBe("myslash_A1"); 32 | }) 33 | 34 | test('id -> StringSelect', () => { 35 | expect(Id.create("mystringselect", CommandType.StringSelect)).toBe("mystringselect_C3"); 36 | }) 37 | 38 | test('id -> UserSelect', () => { 39 | expect(Id.create("myuserselect", CommandType.UserSelect)).toBe("myuserselect_C5"); 40 | }) 41 | 42 | test('id -> RoleSelect', () => { 43 | expect(Id.create("myroleselect", CommandType.RoleSelect)).toBe("myroleselect_C6"); 44 | }) 45 | 46 | test('id -> MentionSelect', () => { 47 | expect(Id.create("mymentionselect", CommandType.MentionableSelect)).toBe("mymentionselect_C7"); 48 | }) 49 | 50 | test('id -> ChannelSelect', () => { 51 | const modal = Id.create("mychannelselect", CommandType.ChannelSelect) 52 | expect(modal).toBe("mychannelselect_C8"); 53 | }) 54 | 55 | test('id reconstruct button', () => { 56 | const idload = Id.reconstruct(new ButtonInteraction("btn")) 57 | expect(idload[0].id).toBe("btn_C2") 58 | }) 59 | 60 | test('id reconstruct button with params', () => { 61 | const idload = Id.reconstruct(new ButtonInteraction("btn/asdf")) 62 | expect(idload[0].id).toBe("btn_C2") 63 | expect(idload[0].params).toBe("asdf") 64 | }) 65 | test('id reconstruct modal with params', () => { 66 | const idload = Id.reconstruct(new ModalSubmitInteraction("btn/asdf")) 67 | expect(idload[0].id).toBe("btn_M") 68 | expect(idload[0].params).toBe("asdf") 69 | }) 70 | test('id reconstruct modal', () => { 71 | const idload = Id.reconstruct(new ModalSubmitInteraction("btn")) 72 | expect(idload[0].id).toBe("btn_M") 73 | expect(idload[0].params).toBe(undefined) 74 | }) 75 | test('id reconstruct button with empty params', () => { 76 | const idload = Id.reconstruct(new ButtonInteraction("btn/")) 77 | expect(idload[0].id).toBe("btn_C2") 78 | expect(idload[0].params).toBe("") 79 | }) 80 | test('id reconstruct with multiple slashes', () => { 81 | const idload = Id.reconstruct(new ButtonInteraction("btn//")) 82 | expect(idload[0].id).toBe("btn_C2") 83 | expect(idload[0].params).toBe("/") 84 | }) 85 | 86 | 87 | test('id reconstruct button', () => { 88 | const idload = Id.reconstruct(new ButtonInteraction("btn")) 89 | expect(idload[0].id).toBe("btn_C2") 90 | expect(idload[0].params).toBe(undefined) 91 | }) 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /test/core/module-loading.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import path from 'node:path' 3 | import * as Files from '../../src/core/module-loading' 4 | import { Module } from '../../src/types/core-modules' 5 | import { AssertionError } from 'node:assert' 6 | //TODO: mock fs? 7 | describe('module-loading', () => { 8 | it('should get the filename of the commandmodule (linux, esm)', () => { 9 | const fname = "///home/pooba/Projects/sern/halibu/dist/commands/ping.js" 10 | const callsiteinfo = Files.parseCallsite(fname) 11 | expect(callsiteinfo.name).toBe("ping") 12 | }) 13 | it('should get filename of commandmodule (linux, cjs)', () => { 14 | const fname = "file:///home/pooba/Projects/sern/halibu/dist/commands/ping.js" 15 | const callsiteinfo = Files.parseCallsite(fname) 16 | expect(callsiteinfo.name).toBe("ping") 17 | 18 | }) 19 | it('should get the filename of the commandmodule (windows, cjs)', () => { 20 | //this test case is impossible on linux. 21 | if(process.platform == 'win32') { 22 | const fname = "C:\\pooba\\Projects\\sern\\halibu\\dist\\commands\\ping.js" 23 | const callsiteinfo = Files.parseCallsite(fname) 24 | expect(callsiteinfo.name).toEqual("ping"); 25 | } 26 | }) 27 | it('should get filename of commandmodule (windows, esm)', () => { 28 | //this test case is impossible on linux. 29 | if(process.platform == 'win32') { 30 | const fname = "file:///C:\\pooba\\Projects\\sern\\halibu\\dist\\commands\\ping.js" 31 | const callsiteinfo = Files.parseCallsite(fname) 32 | expect(callsiteinfo.name).toEqual("ping"); 33 | } 34 | 35 | }) 36 | 37 | it('should import a commandModule properly', async () => { 38 | const { module } = await Files.importModule(path.resolve("test", 'mockules', "module.ts")); 39 | expect(module.name).toBe('module') 40 | }) 41 | it('should throw when failed commandModule import', async () => { 42 | try { 43 | await Files.importModule(path.resolve('test', 'mockules', 'failed.ts')) 44 | } catch(e) { 45 | expect(e instanceof AssertionError) 46 | } 47 | }) 48 | it('should throw when failed commandModule import', async () => { 49 | try { 50 | await Files.importModule(path.resolve('test', 'mockules', 'failed.ts')) 51 | } catch(e) { 52 | expect(e instanceof AssertionError) 53 | } 54 | }) 55 | 56 | it('reads all modules in mockules', async () => { 57 | const ps = [] as string[] 58 | for await (const fpath of Files.readRecursive(path.resolve('test', 'mockules'))) { 59 | ps.push(fpath) 60 | } 61 | expect(ps.length === 4) 62 | }) 63 | 64 | }) 65 | -------------------------------------------------------------------------------- /test/core/presence.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 2 | import { Presence } from '../../src'; 3 | import * as Files from '../../src/core/module-loading' 4 | import { presenceHandler } from '../../src/handlers/presence' 5 | 6 | // Example test suite for the module function 7 | describe('module function', () => { 8 | it('should return a valid configuration', () => { 9 | const config = Presence.module({ 10 | inject: ['dependency1', 'dependency2'], 11 | execute: vi.fn(), 12 | }); 13 | 14 | expect(config).toBeDefined(); 15 | expect(config.inject).toEqual(['dependency1', 'dependency2']); 16 | expect(typeof config.execute).toBe('function'); 17 | }); 18 | }); 19 | 20 | 21 | describe('of function', () => { 22 | it('should return a valid presence configuration without repeat and onRepeat', () => { 23 | const presenceConfig = Presence.of({ 24 | status: 'online', 25 | afk: false, 26 | activities: [{ name: 'Test Activity' }], 27 | shardId: [1, 2, 3], 28 | }).once(); 29 | 30 | expect(presenceConfig).toBeDefined(); 31 | //@ts-ignore Maybe fix? 32 | expect(presenceConfig.repeat).toBeUndefined(); 33 | //@ts-ignore Maybe fix? 34 | expect(presenceConfig.onRepeat).toBeUndefined(); 35 | expect(presenceConfig).toMatchObject({ 36 | status: 'online', 37 | afk: false, 38 | activities: [{ name: 'Test Activity' }], 39 | shardId: [1, 2, 3], 40 | }); 41 | }); 42 | 43 | it('should return a valid presence configuration with repeat and onRepeat', () => { 44 | const onRepeatCallback = vi.fn(); 45 | const presenceConfig = Presence.of({ 46 | status: 'idle', 47 | activities: [{ name: 'Another Test Activity' }], 48 | }).repeated(onRepeatCallback, 5000); 49 | 50 | expect(presenceConfig).toBeDefined(); 51 | expect(presenceConfig.repeat).toBe(5000); 52 | expect(presenceConfig.onRepeat).toBe(onRepeatCallback); 53 | expect(presenceConfig).toMatchObject({ 54 | status: 'idle', 55 | activities: [{ name: 'Another Test Activity' }], 56 | }); 57 | }); 58 | 59 | 60 | }) 61 | 62 | 63 | describe('Presence module execution', () => { 64 | const mockExecuteResult = Presence.of({ 65 | status: 'online', 66 | }).once(); 67 | 68 | const mockModule = Presence.module({ 69 | inject: [ '@sern/client'], 70 | execute: vi.fn().mockReturnValue(mockExecuteResult) 71 | }) 72 | beforeEach(() => { 73 | vi.clearAllMocks(); 74 | // Mock Files.importModule 75 | vi.spyOn(Files, 'importModule').mockResolvedValue({ 76 | module: mockModule 77 | }); 78 | 79 | 80 | 81 | }); 82 | it('should set presence once.', async () => { 83 | const setPresenceMock = vi.fn(); 84 | const mockPath = '/path/to/presence/config'; 85 | 86 | await presenceHandler(mockPath, setPresenceMock); 87 | 88 | expect(Files.importModule).toHaveBeenCalledWith(mockPath); 89 | expect(setPresenceMock).toHaveBeenCalledOnce(); 90 | }) 91 | 92 | }) 93 | -------------------------------------------------------------------------------- /test/handlers.test.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { beforeEach, describe, expect, it, test } from 'vitest'; 3 | import { callInitPlugins } from '../src/handlers/event-utils'; 4 | 5 | import { Client } from 'discord.js' 6 | import { faker } from '@faker-js/faker'; 7 | import { EventEmitter } from 'events'; 8 | import { CommandControlPlugin, CommandType, controller } from '../src'; 9 | import { createRandomModule, createRandomInitPlugin } from './setup/util'; 10 | 11 | 12 | 13 | function mockDeps() { 14 | return { 15 | '@sern/client': new Client(), 16 | '@sern/emitter': new EventEmitter() 17 | } 18 | } 19 | 20 | describe('calling init plugins', async () => { 21 | let deps; 22 | beforeEach(() => { 23 | deps = mockDeps() 24 | }); 25 | 26 | test ('call init plugins', async () => { 27 | const plugins = createRandomInitPlugin('go', { name: "abc" }) 28 | const mod = createRandomModule([plugins]) 29 | const s = await callInitPlugins(mod, deps, false) 30 | expect("abc").equal(s.name) 31 | }) 32 | test('init plugins replace array', async () => { 33 | const plugins = createRandomInitPlugin('go', { opts: [] }) 34 | const plugins2 = createRandomInitPlugin('go', { opts: ['a'] }) 35 | const mod = createRandomModule([plugins, plugins2]) 36 | const s = await callInitPlugins(mod, deps, false) 37 | expect(['a']).deep.equal(s.opts) 38 | }) 39 | 40 | }) 41 | 42 | 43 | 44 | 45 | test('form sdt', async () => { 46 | 47 | const expectedObject = { 48 | "plugin/abc": faker.person.jobArea(), 49 | "plugin2/abc": faker.git.branch(), 50 | "plugin3/cheese": faker.person.jobArea() 51 | } 52 | 53 | const plugin = CommandControlPlugin((ctx,sdt) => { 54 | return controller.next({ "plugin/abc": expectedObject['plugin/abc'] }); 55 | }); 56 | const plugin2 = CommandControlPlugin((ctx,sdt) => { 57 | return controller.next({ "plugin2/abc": expectedObject['plugin2/abc'] }); 58 | }); 59 | const plugin3 = CommandControlPlugin((ctx,sdt) => { 60 | return controller.next({ "plugin3/cheese": expectedObject['plugin3/cheese'] }); 61 | }); 62 | 63 | }) 64 | 65 | 66 | -------------------------------------------------------------------------------- /test/mockules/!ignd.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sern-handler/handler/513ac8edf4d89ef8d6a1ed18ea3d08f31adf7ddb/test/mockules/!ignd.ts -------------------------------------------------------------------------------- /test/mockules/!ignored/ignored.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sern-handler/handler/513ac8edf4d89ef8d6a1ed18ea3d08f31adf7ddb/test/mockules/!ignored/ignored.ts -------------------------------------------------------------------------------- /test/mockules/failed.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sern-handler/handler/513ac8edf4d89ef8d6a1ed18ea3d08f31adf7ddb/test/mockules/failed.ts -------------------------------------------------------------------------------- /test/mockules/module.ts: -------------------------------------------------------------------------------- 1 | import { CommandType, commandModule } from '../../src/' 2 | export default commandModule({ 3 | type: CommandType.Both, 4 | description: "", 5 | execute: (Ctx, args) => {} 6 | }) 7 | -------------------------------------------------------------------------------- /test/mockules/ug/pass.ts: -------------------------------------------------------------------------------- 1 | import { CommandType, commandModule } from '../../../src/' 2 | export default commandModule({ 3 | type: CommandType.Both, 4 | description: "", 5 | execute: (Ctx, args) => {} 6 | }) 7 | -------------------------------------------------------------------------------- /test/setup/setup-tests.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | import { makeDependencies } from '../../src'; 3 | import { Client } from 'discord.js'; 4 | 5 | vi.mock('discord.js', async (importOriginal) => { 6 | const mod = await importOriginal() 7 | const ModalSubmitInteraction = class { 8 | customId; 9 | type = 5; 10 | isModalSubmit = vi.fn(); 11 | constructor(customId) { 12 | this.customId = customId; 13 | } 14 | }; 15 | const ButtonInteraction = class { 16 | customId; 17 | type = 3; 18 | componentType = 2; 19 | isButton = vi.fn(); 20 | constructor(customId) { 21 | this.customId = customId; 22 | } 23 | }; 24 | const AutocompleteInteraction = class { 25 | type = 4; 26 | option: string; 27 | constructor(s: string) { 28 | this.option = s; 29 | } 30 | options = { 31 | getFocused: vi.fn(), 32 | getSubcommand: vi.fn(), 33 | }; 34 | }; 35 | 36 | return { 37 | Client : vi.fn(), 38 | Collection: mod.Collection, 39 | ComponentType: mod.ComponentType, 40 | InteractionType: mod.InteractionType, 41 | ApplicationCommandOptionType: mod.ApplicationCommandOptionType, 42 | ApplicationCommandType: mod.ApplicationCommandType, 43 | ModalSubmitInteraction, 44 | ButtonInteraction, 45 | AutocompleteInteraction, 46 | ChatInputCommandInteraction: vi.fn() 47 | }; 48 | }); 49 | 50 | await makeDependencies(({ add }) => { 51 | add('@sern/client', { }) 52 | }) 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/setup/util.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker" 2 | import { CommandInitPlugin, CommandType, Module, controller } from "../../src" 3 | import { Processed } from "../../src/types/core-modules" 4 | import { vi } from 'vitest' 5 | 6 | export function createRandomInitPlugin (s: 'go', mut?: Partial) { 7 | return CommandInitPlugin(({ module }) => { 8 | if(mut) { 9 | Object.entries(mut).forEach(([k, v]) => { 10 | module[k] = v 11 | }) 12 | } 13 | return s == 'go' 14 | ? controller.next() 15 | : controller.stop() 16 | }) 17 | } 18 | 19 | export function createRandomModule(plugins: any[]): Processed { 20 | return { 21 | type: CommandType.Both, 22 | meta: { id:"", absPath: "" }, 23 | description: faker.string.alpha(), 24 | plugins, 25 | name: "cheese", 26 | onEvent: [], 27 | locals: {}, 28 | execute: vi.fn(), 29 | }; 30 | } 31 | 32 | 33 | export function createRandomChoice() { 34 | return { 35 | type: faker.number.int({ min: 1, max: 11 }), 36 | name: faker.word.noun(), 37 | description: faker.word.adjective(), 38 | }; 39 | } 40 | 41 | 42 | export function createRandomPlugins(len: number) { 43 | const random = () => Math.floor(Math.random() * 2) + 1; // 1 or 2, plugin enum 44 | return Array.from({ length: len }, () => ({ 45 | type: random(), 46 | execute: () => (random() === 1 ? controller.next() : controller.stop()), 47 | })); 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "strictNullChecks": true, 7 | "moduleResolution": "node16", 8 | "skipLibCheck": true, 9 | "declaration": true, 10 | "preserveSymlinks": true, 11 | "allowSyntheticDefaultImports": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "isolatedModules": true, 14 | "outDir": "dist", 15 | "module": "node16", 16 | "target": "esnext", 17 | "sourceMap": true 18 | }, 19 | "exclude": ["node_modules", "dist"], 20 | "include": ["./src", "./src/**/*.d.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | // vitest.config.ts or vitest.config.js 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | setupFiles: ['./test/setup/setup-tests.ts'], 7 | }, 8 | }) 9 | --------------------------------------------------------------------------------