├── .eslintrc.json ├── .eslintrcmd.json ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── ISSUE.md │ └── PROPOSAL.md ├── PULL_REQUEST_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE │ ├── BUG_FIX.md │ ├── CHANGES.md │ ├── DOCUMENTATION.md │ └── FEATURES.md └── workflows │ ├── build.yml │ ├── codequality.yml │ └── test.yml ├── .gitignore ├── .nycrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── guides ├── .docconfig.json ├── Advanced Commands │ ├── CommandsArguments.md │ ├── CommandsCustomResolvers.md │ ├── CommandsCustomResponses.md │ └── CommandsSubcommands.md ├── Advanced SettingsGateway │ ├── SettingsGatewayKeyTypes.md │ ├── SettingsGatewaySettingsUpdate.md │ ├── UnderstandingSchemaFolders.md │ └── UnderstandingSchemaPieces.md ├── Building Your Bot │ └── CreatingPointsSystems.md ├── Getting Started │ ├── GettingStarted.md │ ├── UnderstandingPermissionLevels.md │ ├── UnderstandingSettingsGateway.md │ ├── UnderstandingUsageStrings.md │ └── faq.md ├── Other Subjects │ ├── PieceStores.md │ ├── Plugins.md │ ├── RichDisplay.md │ └── RichMenu.md └── Piece Basics │ ├── CreatingArguments.md │ ├── CreatingCommands.md │ ├── CreatingEvents.md │ ├── CreatingExtendables.md │ ├── CreatingFinalizers.md │ ├── CreatingInhibitors.md │ ├── CreatingLanguages.md │ ├── CreatingMonitors.md │ ├── CreatingProviders.md │ ├── CreatingSQLProviders.md │ ├── CreatingSerializers.md │ └── CreatingTasks.md ├── package.json ├── src ├── arguments │ ├── argument.ts │ ├── arguments.ts │ ├── boolean.ts │ ├── channel.ts │ ├── channels.ts │ ├── command.ts │ ├── commands.ts │ ├── custom.ts │ ├── date.ts │ ├── default.ts │ ├── dmChannel.ts │ ├── dmChannels.ts │ ├── duration.ts │ ├── emoji.ts │ ├── emojis.ts │ ├── event.ts │ ├── events.ts │ ├── extendable.ts │ ├── extendables.ts │ ├── finalizer.ts │ ├── finalizers.ts │ ├── float.ts │ ├── guild.ts │ ├── guilds.ts │ ├── hyperlink.ts │ ├── inhibitor.ts │ ├── inhibitors.ts │ ├── integer.ts │ ├── language.ts │ ├── languages.ts │ ├── literal.ts │ ├── member.ts │ ├── members.ts │ ├── message.ts │ ├── messages.ts │ ├── monitor.ts │ ├── monitors.ts │ ├── piece.ts │ ├── pieces.ts │ ├── provider.ts │ ├── providers.ts │ ├── regexp.ts │ ├── restString.ts │ ├── role.ts │ ├── roles.ts │ ├── store.ts │ ├── stores.ts │ ├── string.ts │ ├── task.ts │ ├── tasks.ts │ ├── textChannel.ts │ ├── textChannels.ts │ ├── time.ts │ ├── user.ts │ ├── users.ts │ ├── voiceChannel.ts │ └── voiceChannels.ts ├── commands │ ├── Admin │ │ ├── conf.ts │ │ ├── disable.ts │ │ ├── enable.ts │ │ ├── eval.ts │ │ ├── load.ts │ │ ├── reboot.ts │ │ ├── reload.ts │ │ ├── transfer.ts │ │ └── unload.ts │ └── General │ │ ├── Chat Bot Info │ │ ├── help.ts │ │ ├── info.ts │ │ ├── invite.ts │ │ ├── ping.ts │ │ └── stats.ts │ │ └── User Settings │ │ └── userconf.ts ├── events │ ├── argumentError.ts │ ├── commandError.ts │ ├── commandInhibited.ts │ ├── coreGuildDelete.ts │ ├── coreMessageCreate.ts │ ├── coreMessageDelete.ts │ ├── coreMessageDeleteBulk.ts │ ├── coreMessageUpdate.ts │ ├── debug.ts │ ├── disconnect.ts │ ├── error.ts │ ├── eventError.ts │ ├── finalizerError.ts │ ├── log.ts │ ├── monitorError.ts │ ├── taskError.ts │ ├── unhandledRejection.ts │ ├── verbose.ts │ ├── warn.ts │ └── wtf.ts ├── finalizers │ └── commandLogging.ts ├── index.ts ├── inhibitors │ ├── disabled.ts │ ├── hidden.ts │ ├── missingBotPermissions.ts │ ├── nsfw.ts │ ├── permissions.ts │ ├── requiredSettings.ts │ ├── runIn.ts │ └── slowmode.ts ├── languages │ └── en-US.ts ├── lib │ ├── Client.ts │ ├── extensions │ │ ├── KlasaGuild.ts │ │ ├── KlasaMessage.ts │ │ └── KlasaUser.ts │ ├── permissions │ │ └── PermissionLevels.ts │ ├── schedule │ │ ├── Schedule.ts │ │ └── ScheduledTask.ts │ ├── settings │ │ ├── Settings.ts │ │ ├── gateway │ │ │ ├── Gateway.ts │ │ │ └── GatewayStore.ts │ │ └── schema │ │ │ ├── Schema.ts │ │ │ └── SchemaEntry.ts │ ├── structures │ │ ├── Argument.ts │ │ ├── ArgumentStore.ts │ │ ├── Command.ts │ │ ├── CommandStore.ts │ │ ├── Extendable.ts │ │ ├── ExtendableStore.ts │ │ ├── Finalizer.ts │ │ ├── FinalizerStore.ts │ │ ├── Inhibitor.ts │ │ ├── InhibitorStore.ts │ │ ├── Language.ts │ │ ├── LanguageStore.ts │ │ ├── Monitor.ts │ │ ├── MonitorStore.ts │ │ ├── MultiArgument.ts │ │ ├── Provider.ts │ │ ├── ProviderStore.ts │ │ ├── SQLProvider.ts │ │ ├── Serializer.ts │ │ ├── SerializerStore.ts │ │ ├── Task.ts │ │ └── TaskStore.ts │ ├── usage │ │ ├── CommandPrompt.ts │ │ ├── CommandUsage.ts │ │ ├── Possible.ts │ │ ├── Tag.ts │ │ ├── TextPrompt.ts │ │ └── Usage.ts │ └── util │ │ ├── QueryBuilder.ts │ │ ├── ReactionHandler.ts │ │ ├── RichDisplay.ts │ │ ├── RichMenu.ts │ │ └── constants.ts ├── monitors │ └── commandHandler.ts ├── providers │ └── json.ts └── serializers │ ├── any.ts │ ├── boolean.ts │ ├── channel.ts │ ├── guild.ts │ ├── number.ts │ ├── piece.ts │ ├── role.ts │ ├── string.ts │ ├── url.ts │ └── user.ts ├── test ├── Gateway.ts ├── GatewayStore.ts ├── ProviderStore.ts ├── Schema.ts ├── SchemaEntry.ts ├── Settings.ts ├── lib │ ├── MockLanguage.ts │ ├── MockNumberSerializer.ts │ ├── MockObjectSerializer.ts │ ├── MockProvider.ts │ ├── MockStringSerializer.ts │ └── SettingsClient.ts └── test.ts ├── tsconfig.json ├── typedoc.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "klasa/eslint-ts" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrcmd.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "klasa/md" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Dirigeant's Code of Conduct 2 | 3 | Participation in this project and/or community indicate agreement to the [Dirigeant's Code of Conduct](https://github.com/dirigeants/CoC/blob/master/CODE_OF_CONDUCT.md) 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **The issue tracker is only for issue reporting or proposals/suggestions. If you have a question, you can find us in our [Discord Server](https://discord.gg/FpEFSyY)**. 4 | 5 | To contribute to this repository, feel free to create a new fork of the repository 6 | submit a pull request. We highly suggest [ESLint](https://eslint.org/) to be installed 7 | in your text editor or IDE of your choice to avoid fail builds from Travis. 8 | 9 | 1. Fork, clone, and select the **master** branch. 10 | 2. Create a new branch in your fork. 11 | 3. Commit your changes, and push them. 12 | 4. Submit a Pull Request [here](https://github.com/dirigeants/klasa/pulls)! 13 | 14 | ## Klasa Concept Guidelines 15 | 16 | There are a number of guidelines considered when reviewing Pull Requests to be merged into core framework (further referred to as __core__). _This is by no means an exhaustive list, but here are some things to consider before/while submitting your ideas._ 17 | 18 | - Klasa should never change D.JS's default behavior in core. Klasa should only add to D.JS and be as consistent as possible with D.JS. 19 | - Nothing in core should respond with embeds or be terribly "personalized". Instead everything should be an abstract working base that people can personalize themselves to their own needs. 20 | - Everything in core should be generally useful for the majority of Klasa bots. (A reason why core doesn't implement any Music features.) Don't let that stop you if you've got a good concept though, as your idea still might be a great addition to [klasa-pieces](https://github.com/dirigeants/klasa-pieces) or as an optional addon package. 21 | - As much of the framework as possible is meant to be customizable for any possible use-case, even if the use-case is niche. New features shouldn't break existing use-cases without a strong/well-thought-out reason (that doesn't conflict with any other guideline). 22 | - Everything should be shard compliant. If something you are PRing would break when sharding, break other things from supporting sharding, or is incompatible with sharding; then you will need to think of a way to make it work with sharding in mind before it will be accepted/merged. 23 | - Everything should be documented with [jsdocs](http://usejsdoc.org/), whether private or not. __If you see a mistake in the docs, please pr a fix.__ 24 | - Everything should follow OOP paradigms and generally rely on behavior over state where possible. This generally helps methods be predictable, keeps the codebase simple & understandable, reduces code duplication through abstraction, and leads to efficiency and therefore scalability. 25 | - Everything should follow our ESLint rules as closely as possible, and should pass lint tests even if you must disable a rule for a single line. 26 | - Core should follow [Discord Bot Best Practices](https://github.com/meew0/discord-bot-best-practices) 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [bdistin] 4 | patreon: klasa 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Describe the issue 2 | 3 | ### Code or steps to reproduce 4 | 5 | ```js 6 | 7 | ``` 8 | 9 | 14 | 15 | ### Expected and actual behavior 16 | 17 | ### Further details 18 | 19 | - **@klasa/core version**: 20 | - **node.js version**: 21 | - **Klasa version**: 22 | - [ ] I have modified core files. 23 | - [ ] I have tested the issue on latest master. Commit hash: 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ISSUE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Submit an issue 3 | about: Describe us an issue you've received with Klasa. 4 | 5 | --- 6 | 7 | 12 | 13 | ### Describe the issue 14 | 15 | ### Code or steps to reproduce 16 | 17 | ```js 18 | 19 | ``` 20 | 21 | ### Expected and actual behavior 22 | 23 | 28 | 29 | ### Further details 30 | 31 | - **@klasa/core version**: 32 | - **node.js version**: 33 | - **Klasa version**: 34 | - [ ] I have modified core files. 35 | - [ ] I have tested the issue on latest master. Commit hash: 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/PROPOSAL.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Submit a Proposal 3 | about: Suggest enhancements or improvements to existing features. 4 | 5 | --- 6 | 7 | ### Describe your proposal 8 | 9 | ### Use-Cases for your proposal 10 | 11 | ### Expected and actual behavior 12 | 13 | ### Further details 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description of the PR 2 | 3 | 4 | ### Changes Proposed in this Pull Request (List new items in CHANGELOG.MD) 5 | 6 | - 7 | 8 | ### Semver Classification 9 | 10 | - [ ] This PR only includes documentation or non-code changes. 11 | - [ ] This PR fixes a bug and does not change the (intended) framework interface. 12 | - [ ] This PR adds methods or properties to the framework interface. 13 | - [ ] This PR removes or renames methods or properties in the framework interface. 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/BUG_FIX.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Fix 3 | about: Fixes a bug or more bugs that showed up. 4 | 5 | --- 6 | 7 | ### Description of the PR 8 | 9 | ### Bug(s) fixed 10 | 11 | - 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/CHANGES.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Change or Remove Methods or Properties 3 | about: Modifies or removes currently existing functions 4 | 5 | --- 6 | 7 | ### Description of the PR 8 | 9 | ### Changes done in this Pull Request (List changes in CHANGELOG.MD) 10 | 11 | - 12 | 13 | - [ ] This PR has been tested. 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Correct or Add new Documentations 3 | about: In case some documentations were wrong. 4 | 5 | --- 6 | 7 | ### Description of the documentation change 8 | 9 | ### Changes 10 | 11 | - 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/FEATURES.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Features 3 | about: Adds new features or changes current methods or properties to bring in new functionality. 4 | 5 | --- 6 | 7 | ### Description of the PR 8 | 9 | ### Changes Proposed in this Pull Request (List changes in CHANGELOG.MD) 10 | 11 | - 12 | 13 | - [ ] This PR has been tested. 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | TSC: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Project 14 | uses: actions/checkout@v1 15 | - name: Use Node.js 12 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 12 19 | - name: Restore CI Cache 20 | uses: actions/cache@v1 21 | with: 22 | path: node_modules 23 | key: ${{ runner.os }}-12-${{ hashFiles('**/yarn.lock') }} 24 | - name: Install Dependencies 25 | run: yarn 26 | - name: Build and Push 27 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 28 | run: | 29 | #!/bin/bash 30 | set -euxo pipefail 31 | echo -e "\n# Initialize some useful variables" 32 | REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 33 | BRANCH_OR_TAG=`awk -F/ '{print $2}' <<< $GITHUB_REF` 34 | CURRENT_BRANCH=`awk -F/ '{print $NF}' <<< $GITHUB_REF` 35 | if [ "$BRANCH_OR_TAG" == "heads" ]; then 36 | SOURCE_TYPE="branch" 37 | else 38 | SOURCE_TYPE="tag" 39 | fi 40 | echo -e "\n# Checkout the repo in the target branch" 41 | TARGET_BRANCH="build" 42 | git clone $REPO out -b $TARGET_BRANCH 43 | yarn build 44 | rm -rfv out/dist/* 45 | echo -e "\n# Move the generated docs to the newly-checked-out repo, to be committed and pushed" 46 | mv package.json out/package.json 47 | mv LICENSE out/LICENSE 48 | rsync -vau dist/src out/dist 49 | echo -e "\n# Commit and push" 50 | cd out 51 | git add --all . 52 | git config user.name "${GITHUB_ACTOR}" 53 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 54 | git commit -m "TSC build: ${GITHUB_SHA}" || true 55 | git push origin $TARGET_BRANCH 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.COMMIT_TOKEN }} 58 | 59 | TypeDocs: 60 | name: TypeDocs(temp) 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout Project 64 | uses: actions/checkout@v1 65 | - name: Use Node.js 12 66 | uses: actions/setup-node@v1 67 | with: 68 | node-version: 12 69 | - name: Restore CI Cache 70 | uses: actions/cache@v1 71 | with: 72 | path: node_modules 73 | key: ${{ runner.os }}-12-${{ hashFiles('**/yarn.lock') }} 74 | - name: Install Dependencies 75 | run: yarn 76 | - name: Publish Docs 77 | run: | 78 | #!/bin/bash 79 | set -euxo pipefail 80 | 81 | echo -e "\n# Initialise some useful variables" 82 | REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 83 | BRANCH_OR_TAG=`awk -F/ '{print $2}' <<< $GITHUB_REF` 84 | CURRENT_BRANCH=`awk -F/ '{print $NF}' <<< $GITHUB_REF` 85 | 86 | if [ "$BRANCH_OR_TAG" == "heads" ]; then 87 | SOURCE_TYPE="branch" 88 | else 89 | SOURCE_TYPE="tag" 90 | fi 91 | 92 | echo -e "\n# Checkout the repo in the target branch" 93 | TARGET_BRANCH="gh-pages" 94 | git clone $REPO out -b $TARGET_BRANCH 95 | 96 | yarn docs:html 97 | 98 | echo -e "\n# Move the generated docs to the newly-checked-out repo, to be committed and pushed" 99 | rsync -vau docs/ out/ 100 | 101 | echo -e "\n# Commit and push" 102 | cd out 103 | git add --all . 104 | git config user.name "${GITHUB_ACTOR}" 105 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 106 | git commit -m "Docs build: ${GITHUB_SHA}" || true 107 | git push origin $TARGET_BRANCH 108 | env: 109 | GITHUB_TOKEN: ${{ secrets.COMMIT_TOKEN }} 110 | -------------------------------------------------------------------------------- /.github/workflows/codequality.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - stable 8 | pull_request: 9 | 10 | jobs: 11 | ESLint: 12 | name: ESLint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Project 16 | uses: actions/checkout@v1 17 | - name: Use Node.js 12 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | - name: Restore CI Cache 22 | uses: actions/cache@v1 23 | with: 24 | path: node_modules 25 | key: ${{ runner.os }}-12-${{ hashFiles('**/yarn.lock') }} 26 | - name: Install Dependencies 27 | run: yarn 28 | - name: Run ESLint 29 | uses: icrawl/action-eslint@v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | job-name: ESLint 34 | 35 | Typescript: 36 | name: Typescript 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout Project 40 | uses: actions/checkout@v1 41 | - name: Use Node.js 12 42 | uses: actions/setup-node@v1 43 | with: 44 | node-version: 12 45 | - name: Restore CI Cache 46 | uses: actions/cache@v1 47 | with: 48 | path: node_modules 49 | key: ${{ runner.os }}-12-${{ hashFiles('**/yarn.lock') }} 50 | - name: Install Dependencies 51 | run: yarn 52 | - name: Run TSC 53 | uses: icrawl/action-tsc@v1 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | with: 57 | job-name: Typescript 58 | 59 | TypeDocs: 60 | name: TypeDocs 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout Project 64 | uses: actions/checkout@v1 65 | - name: Use Node.js 12 66 | uses: actions/setup-node@v1 67 | with: 68 | node-version: 12 69 | - name: Restore CI Cache 70 | uses: actions/cache@v1 71 | with: 72 | path: node_modules 73 | key: ${{ runner.os }}-12-${{ hashFiles('**/yarn.lock') }} 74 | - name: Install Dependencies 75 | run: yarn 76 | - name: Test Docs 77 | if: github.event_name == 'pull_request' 78 | run: yarn docs 79 | - name: Publish Docs 80 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 81 | run: | 82 | #!/bin/bash 83 | set -euxo pipefail 84 | echo -e "\n# Initialize some useful variables" 85 | REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 86 | BRANCH_OR_TAG=`awk -F/ '{print $2}' <<< $GITHUB_REF` 87 | CURRENT_BRANCH=`awk -F/ '{print $NF}' <<< $GITHUB_REF` 88 | if [ "$BRANCH_OR_TAG" == "heads" ]; then 89 | SOURCE_TYPE="branch" 90 | else 91 | SOURCE_TYPE="tag" 92 | fi 93 | echo -e "\n# Checkout the repo in the target branch" 94 | TARGET_BRANCH="docs" 95 | git clone $REPO out -b $TARGET_BRANCH 96 | yarn docs 97 | echo -e "\n# Move the generated docs to the newly-checked-out repo, to be committed and pushed" 98 | mv docs.json out/${CURRENT_BRANCH//\//_}.json 99 | echo -e "\n# Commit and push" 100 | cd out 101 | git add --all . 102 | git config user.name "${GITHUB_ACTOR}" 103 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 104 | git commit -m "Docs build: ${GITHUB_SHA}" || true 105 | git push origin $TARGET_BRANCH 106 | env: 107 | GITHUB_TOKEN: ${{ secrets.COMMIT_TOKEN }} 108 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - stable 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | name: Node v${{ matrix.node_version }} - ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | node_version: [12, 14] 17 | os: [ubuntu-latest, windows-latest, macOS-latest] 18 | 19 | steps: 20 | - name: Checkout Project 21 | uses: actions/checkout@v1 22 | - name: Use Node.js ${{ matrix.node_version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node_version }} 26 | - name: Restore CI Cache 27 | uses: actions/cache@v1 28 | with: 29 | path: node_modules 30 | key: ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles(matrix.os == 'windows-latest' && '**\yarn.lock' || '**/yarn.lock') }} 31 | - name: Install Dependencies 32 | run: yarn 33 | - name: Build Project 34 | run: yarn build 35 | - name: Test 36 | run: yarn coverage 37 | - uses: actions/upload-artifact@v1 38 | name: Upload Coverage Data 39 | with: 40 | name: ${{ runner.os }}-${{ matrix.node_version }} 41 | path: .nyc_output 42 | report: 43 | needs: test 44 | name: Generate Report 45 | runs-on: windows-latest 46 | 47 | steps: 48 | - name: Checkout Project 49 | uses: actions/checkout@v1 50 | - name: Use Node.js 12 51 | uses: actions/setup-node@v1 52 | with: 53 | node-version: 12 54 | - name: Restore CI Cache 55 | uses: actions/cache@v1 56 | with: 57 | path: node_modules 58 | key: Windows-12-${{ hashFiles('**\yarn.lock') }} 59 | - name: Install Dependencies 60 | run: yarn 61 | - uses: actions/download-artifact@v1 62 | name: Download Windows-12 Coverage Data 63 | with: 64 | name: Windows-12 65 | path: .nyc_output 66 | - uses: actions/download-artifact@v1 67 | name: Download Windows-14 Coverage Data 68 | with: 69 | name: Windows-14 70 | path: .nyc_output 71 | - uses: actions/download-artifact@v1 72 | name: Download macOS-12 Coverage Data 73 | with: 74 | name: macOS-12 75 | path: .nyc_output 76 | - uses: actions/download-artifact@v1 77 | name: Download macOS-14 Coverage Data 78 | with: 79 | name: macOS-14 80 | path: .nyc_output 81 | - uses: actions/download-artifact@v1 82 | name: Download Linux-12 Coverage Data 83 | with: 84 | name: Linux-12 85 | path: .nyc_output 86 | - uses: actions/download-artifact@v1 87 | name: Download Linux-14 Coverage Data 88 | with: 89 | name: Linux-14 90 | path: .nyc_output 91 | - name: Report 92 | run: yarn coverage:report 93 | - uses: actions/upload-artifact@v1 94 | name: Upload Report 95 | with: 96 | name: report 97 | path: coverage 98 | #- name: Test Cross Platform Coverage 99 | #run: yarn test:coverage 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | 4 | out/ 5 | coverage/ 6 | .nyc_output/ 7 | dist/ 8 | docs/ 9 | test-results.xml 10 | 11 | test.js 12 | 13 | config.json 14 | 15 | .vscode/ 16 | .idea/ 17 | .vs/ 18 | yarn-error.log 19 | docs.json 20 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "check-coverage": false, 4 | "watermarks": { 5 | "lines": [ 6 | 80, 7 | 95 8 | ], 9 | "functions": [ 10 | 80, 11 | 95 12 | ], 13 | "branches": [ 14 | 80, 15 | 95 16 | ], 17 | "statements": [ 18 | 80, 19 | 95 20 | ] 21 | }, 22 | "exclude": [ 23 | "**/*.d.ts", 24 | "coverage/**", 25 | "packages/*/test/**", 26 | "test/**", 27 | "test{,-*}.ts", 28 | "**/*{.,-}{test,spec}.ts", 29 | "**/__tests__/**", 30 | "**/node_modules/**", 31 | "examples/" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 dirigeants 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 | -------------------------------------------------------------------------------- /guides/.docconfig.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "Getting Started", 3 | "files": [{ 4 | "name": "Getting Started", 5 | "path": "GettingStarted.md" 6 | }, 7 | { 8 | "name": "Understanding Permission Levels", 9 | "path": "UnderstandingPermissionLevels.md" 10 | }, 11 | { 12 | "name": "Understanding SettingsGateway", 13 | "path": "UnderstandingSettingsGateway.md" 14 | }, 15 | { 16 | "name": "Understanding Usage Strings", 17 | "path": "UnderstandingUsageStrings.md" 18 | }, 19 | { 20 | "name": "Frequently Asked Questions", 21 | "path": "faq.md" 22 | }] 23 | }, 24 | { 25 | "name": "Piece Basics", 26 | "files": [ 27 | { 28 | "name": "Creating Arguments", 29 | "path": "CreatingArguments.md" 30 | }, 31 | { 32 | "name": "Creating Commands", 33 | "path": "CreatingCommands.md" 34 | }, 35 | { 36 | "name": "Creating Events", 37 | "path": "CreatingEvents.md" 38 | }, 39 | { 40 | "name": "Creating Extendables", 41 | "path": "CreatingExtendables.md" 42 | }, 43 | { 44 | "name": "Creating Finalizers", 45 | "path": "CreatingFinalizers.md" 46 | }, 47 | { 48 | "name": "Creating Inhibitors", 49 | "path": "CreatingInhibitors.md" 50 | }, 51 | { 52 | "name": "Creating Languages", 53 | "path": "CreatingLanguages.md" 54 | }, 55 | { 56 | "name": "Creating Monitors", 57 | "path": "CreatingMonitors.md" 58 | }, 59 | { 60 | "name": "Creating Providers", 61 | "path": "CreatingProviders.md" 62 | }, 63 | { 64 | "name": "Creating Serializers", 65 | "path": "CreatingSerializers.md" 66 | }, 67 | { 68 | "name": "Creating SQL Providers", 69 | "path": "CreatingSQLProviders.md" 70 | }, 71 | { 72 | "name": "Creating Tasks", 73 | "path": "CreatingTasks.md" 74 | }] 75 | }, 76 | { 77 | "name": "Advanced Commands", 78 | "files": [{ 79 | "name": "Commands I: Arguments", 80 | "path": "CommandsArguments.md" 81 | }, 82 | { 83 | "name": "Commands II: Subcommands", 84 | "path": "CommandsSubcommands.md" 85 | }, 86 | { 87 | "name": "Commands III: Per-Command and per-parameter custom responses", 88 | "path": "CommandsCustomResponses.md" 89 | }, 90 | { 91 | "name": "Commands IV: Per-Command custom argument resolvers", 92 | "path": "CommandsCustomResolvers.md" 93 | }] 94 | }, 95 | { 96 | "name": "Advanced SettingsGateway", 97 | "files": [{ 98 | "name": "Understanding SchemaFolders", 99 | "path": "UnderstandingSchemaFolders.md" 100 | }, 101 | { 102 | "name": "Understanding SchemaPieces", 103 | "path": "UnderstandingSchemaPieces.md" 104 | }, 105 | { 106 | "name": "SettingsGateway's Types", 107 | "path": "SettingsGatewayKeyTypes.md" 108 | }, 109 | { 110 | "name": "Updating your Settings", 111 | "path": "SettingsGatewaySettingsUpdate.md" 112 | }] 113 | }, 114 | { 115 | "name": "Other Subjects", 116 | "files": [{ 117 | "name": "Piece Stores", 118 | "path": "PieceStores.md" 119 | }, 120 | { 121 | "name": "Rich Display", 122 | "path": "RichDisplay.md" 123 | }, 124 | { 125 | "name": "Rich Menu", 126 | "path": "RichMenu.md" 127 | }, 128 | { 129 | "name": "Plugins", 130 | "path": "Plugins.md" 131 | }] 132 | }, 133 | { 134 | "name": "Building Your Bot", 135 | "files": [{ 136 | "name": "Creating a Points System", 137 | "path": "CreatingPointsSystems.md" 138 | }] 139 | }] 140 | -------------------------------------------------------------------------------- /guides/Advanced Commands/CommandsCustomResolvers.md: -------------------------------------------------------------------------------- 1 | > This feature is implemented in Klasa **0.5.0**, check the PR that implemented it [here](https://github.com/dirigeants/klasa/pull/162). 2 | 3 | ## Custom Resolvers 4 | 5 | Custom resolvers allow developers to set up custom types for each command, they are highly customizable and can hold its own logic and type name. This is possible thanks to the {@link Command.createCustomResolver} method. 6 | 7 | ## Creating a custom Command Resolver 8 | 9 | A custom resolver is usually created in the command constructor and its usage is identical to {@link ArgResolver} methods: 10 | 11 | ```javascript 12 | this.createCustomResolver('key', (arg, possible, message, params) => { 13 | // Logic 14 | }); 15 | ``` 16 | 17 | Where the first parameter is the name of the custom type, and the second is a function that takes `arg` (string), `possible` ({@link Possible}), `message` ({@link KlasaMessage}) and optionally, `params` (any[], remember parameters are parsed arguments). 18 | 19 | Then in your usage, you can use the type `key`, it'll be recognized as a *local* resolver that your command is able to use. Check a live example [here](https://github.com/dirigeants/klasa/blob/c47891581806e64ebf53706231a69037d70dd077/src/commands/Admin/conf.js#L5-L25). You can also check the tutorial {@tutorial CreatingCustomArguments} for further information. 20 | 21 | ## Further Reading: 22 | 23 | - {@tutorial CommandsArguments} 24 | - {@tutorial CommandsSubcommands} 25 | - {@tutorial CommandsCustomResponses} 26 | -------------------------------------------------------------------------------- /guides/Advanced SettingsGateway/SettingsGatewaySettingsUpdate.md: -------------------------------------------------------------------------------- 1 | # Updating your configuration 2 | 3 | Once we have our schema completed with all the keys, folders and types needed, we may want to update our configuration via SettingsGateway, all of this is done via {@link Settings#update}. However, how can I update it? Use any of the following code snippets: 4 | 5 | ```javascript 6 | // Updating the value of a key 7 | // This key is contained in the roles folder, and the second value is a role id, we also need 8 | // to pass a GuildResolvable. 9 | message.guild.settings.update('roles.administrator', '339943234405007361', message.guild); 10 | 11 | // For retrocompatibility, the object overload is still available, however, this is much slower. 12 | // If you store objects literals in keys that do not take an array, this may break, prefer the 13 | // other overload or use nested SchemaPieces for full consistency. 14 | message.guild.settings.update({ roles: { administrator: '339943234405007361' } }, message.guild); 15 | 16 | // Updating an array 17 | // userBlacklist, as mentioned in another tutorial, it's a piece with an array of users. Using 18 | // the following code will add or remove it, depending on the existence of the key in the configuration. 19 | message.guild.settings.update('userBlacklist', '272689325521502208'); 20 | 21 | // Ensuring the function call adds (error if it exists) 22 | message.guild.settings.update('userBlacklist', '272689325521502208', { action: 'add' }); 23 | 24 | // Ensuring the function call removes (error if it doesn't exist) 25 | message.guild.settings.update('userBlacklist', '272689325521502208', { action: 'remove' }); 26 | 27 | // Updating multiple keys 28 | message.guild.settings.update([['prefix', 'k!'], ['language', 'es-ES']]); 29 | ``` 30 | 31 | > **Note**: Some types require a Guild instance to work, for example, *channels*, *roles* and *members*. 32 | 33 | > Additionally, if no 'action' option is passed to {@link SettingsUpdateOptions}, it'll assume the `auto` mode, which will add or remove depending on the existence of the key. 34 | 35 | ## Further Reading: 36 | 37 | - {@tutorial UnderstandingSchemaPieces} 38 | - {@tutorial UnderstandingSchemaFolders} 39 | - {@tutorial SettingsGatewayKeyTypes} 40 | -------------------------------------------------------------------------------- /guides/Advanced SettingsGateway/UnderstandingSchemaPieces.md: -------------------------------------------------------------------------------- 1 | # Understanding Schema's Keys 2 | 3 | As mentioned in the previous tutorial, {@tutorial UnderstandingSchema}, SettingsGateway's schema is divided in two parts: **folders** and **pieces**. Pieces are contained in folders, but they cannot have keys nor folders. Instead, this holds the key's metadata such as its type, if it's configurable by the configuration command... you can check more information in the documentation: {@link SchemaPiece}. 4 | 5 | ## Key options 6 | 7 | There are multiple options that configure the piece, they are: 8 | 9 | | Option | Description | 10 | | ------------ | -------------------------------------------------------------------------- | 11 | | array | Whether the values should be stored in an array | 12 | | configurable | Whether this key can be configured with the built-in configuration command | 13 | | default | The default value for this key | 14 | | max | The maximum value for this key, only applies for string and numbers | 15 | | min | The minimum value for this key, only applies for string and numbers | 16 | | filter | The filter function for this key | 17 | 18 | > Check {@tutorial SettingsGatewayKeyTypes} for the supported types and how to extend them. 19 | 20 | ## Default option 21 | 22 | *The default option is optional, but, what is its default value?* 23 | 24 | The default option is one of the last options to default, **array** defaults to `false`, **max** and **min** defaults to `null`, **configurable** defaults to either `true` or `false`, the latter if **type** is `any`; and **type** is always obligatory. 25 | 26 | - If **array** is true, default will be an empty array: `[]`. 27 | - If **type** is boolean, default will be `false`. 28 | - In any other case, it will be `null`. 29 | 30 | ## Filter option 31 | 32 | The filter option serves to blacklist certain values. It's output is not used, but any thrown error will be handled by SettingsGateway's internals and displayed to the caller (for example in the conf command, it would display the message to the user). It also must be synchronous. 33 | 34 | Internally, we use this option to avoid users from disabling guarded commands (check {@link Command#guard}): 35 | 36 | ```javascript 37 | const filter = (client, command, piece, guild) => { 38 | if (client.commands.get(command).guarded) { 39 | throw (guild ? guild.language : client.languages.default).get('COMMAND_CONF_GUARDED', command); 40 | } 41 | }; 42 | ``` 43 | 44 | In this case, `client` is the {@link KlasaClient} instance, `command` the resolved command (the output from the command's SchemaType), `piece` is a {@link SchemaPiece} instance, and guild is a {@link Guild} instance, which may be null. 45 | 46 | ## Further Reading: 47 | 48 | - {@tutorial UnderstandingSchemaFolders} 49 | - {@tutorial SettingsGatewayKeyTypes} 50 | - {@tutorial SettingsGatewaySettingsUpdate} 51 | -------------------------------------------------------------------------------- /guides/Getting Started/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Welcome to the klasa#{@branch} documentation 2 | 3 | ## Installing Klasa 4 | 5 | You can install this branch of klasa with the following command: 6 | 7 | ```sh 8 | npm install --save @klasa/core dirigeants/klasa#{@branch} 9 | ``` 10 | 11 | ### Using Klasa 12 | 13 | Create a file called `app.js` (or whatever you prefer) which will initiate and configure Klasa. 14 | 15 | ```javascript 16 | const { Client } = require('klasa'); 17 | 18 | new Client({ 19 | fetchAllMembers: false, 20 | prefix: '+', 21 | commandEditing: true, 22 | typing: true, 23 | readyMessage: (client) => `Successfully initialized. Ready to serve ${client.guilds.cache.size} guilds.` 24 | }).login('your-bot-token'); 25 | ``` 26 | 27 | ### Client Options: {@link KlasaClientOptions} 28 | 29 | {@typedef KlasaClientOptions} 30 | 31 | >1. ownerID is acquired from the Discord API if not provided: `client.application.owner.id` 32 | >1. quotedStringSupport is can be overridden per command. 33 | 34 | > KlasaClientOptions are merged with @klasa/core's ClientOptions, see [ClientOptions in the discord.js docs](https://discord.js.org/#/docs/main/master/typedef/ClientOptions). 35 | 36 | ## Running the bot 37 | 38 | Then, run the following in your folder: 39 | 40 | ```sh 41 | npm install 42 | node app.js 43 | ``` 44 | 45 | > **Requirements**: Requires Node 10.1.0 or higher to run. Depends on Discord.js v12.0.0-dev or higher (this is peer depended on, so you can choose a non-broken commit). 46 | 47 | ## What's next? 48 | 49 | Klasa will create folders in your directory to make your own custom pieces in. Klasa will automatically check those folders on bootup, or if you reload all of a piece's type. `+reload commands` etc assuming your prefix is `+` like in the example app.js file above. 50 | -------------------------------------------------------------------------------- /guides/Getting Started/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## Why doesn't my [reboot](https://github.com/dirigeants/klasa/blob/master/src/commands/Admin/reboot.js) command work? 4 | 5 | The reboot command calls [process.exit()](https://nodejs.org/api/process.html#process_process_exit_code), which terminates the process of your bot. For it to automatically turn back on, you need to install a process manager. 6 | 7 | A commonly used process manager is [PM2](http://pm2.keymetrics.io/), to set it up for to use for your bot, follow these two steps: 8 | 9 | 1. Run `npm install pm2 -g` 10 | 1. Start your bot using `pm2 start app.js`, where `app.js` is your main bot file. 11 | 12 | For more information on PM2, check out their [documentation](http://pm2.keymetrics.io/docs/usage/quick-start/) or for a very quick summary, the [Cheat Sheet](http://pm2.keymetrics.io/docs/usage/quick-start/#cheatsheet) 13 | 14 | ## How do I remove or change a command that's included in Klasa? 15 | 16 | If you want to disable or modify a piece (commands included) which is built into Klasa, you can use the [transfer](https://github.com/dirigeants/klasa/blob/master/src/commands/Admin/transfer.js) command. 17 | 18 | Run `+transfer `, where `` is the name of the command/piece that you want to modify. 19 | 20 | This will create a copy of the piece into your directory which will override the built-in one, and you can now modify it however you like. 21 | 22 | To disable it, you can set the [enabled option](https://klasa.js.org/#/docs/klasa/master/search?q=enabled) to `false`. 23 | 24 | ## How do I hide my token from Github? 25 | 26 | You should never expose your token to anyone for any reason. To hide it from Github, you can set it as an [environment variable](https://www.twilio.com/blog/2017/08/working-with-environment-variables-in-node-js.html), or place it in a JSON file. 27 | 28 | ### JSON File 29 | 30 | 1. Create a `config.json` file next to your `app.js` file, and paste the following JSON into it: 31 | 32 | ```json 33 | { 34 | "token": "" 35 | } 36 | ``` 37 | 38 | Then, copy your token into the field. 39 | 40 | 1. In your `.gitignore` file (create one if you don't have it), add `config.json` to it. For information on what the `.gitignore` file is and what it does, visit: . 41 | 42 | 1. At the top of your `app.js` file, import the token from the `config.json` like so: 43 | 44 | ```js 45 | const { token } = require('./config.json'); 46 | ``` 47 | 48 | 1. Finally, **remove your token** from your `app.js` file, and replace it with the `token` variable. 49 | 50 | ### Environment Variable 51 | 52 | For a tutorial 53 | 54 | 1. Set `process.env.DISCORD_TOKEN` equal to your bot's token, you can do this by either 55 | 56 | * using the console to set the environment variable every time you run the bot, by doing `set DISCORD_TOKEN=token` 57 | 58 | * using the [dotenv](https://www.npmjs.com/package/dotenv) package. Run `npm install dotenv --save` 59 | 60 | 1. Put this code at the top of your `app.js` file: 61 | 62 | ```js 63 | require('dotenv').config(); 64 | ``` 65 | 66 | 1. Create a file called `.env` next to your `app.js`, and put this in it: 67 | 68 | ```toml 69 | DISCORD_TOKEN = "" 70 | ``` 71 | 72 | > Place your token after the `=`. 73 | 74 | 1. Finally, **remove your token** from your `app.js` file, so nothing is passed to the login method, discord.js will [automatically use](https://github.com/discordjs/discord.js/blob/249673de6ef8da4585e375ba3f0ea6a5800e7055/src/client/Client.js#L129) the token in the environment variable. 75 | 76 | ```js 77 | client.login(); 78 | ``` 79 | -------------------------------------------------------------------------------- /guides/Other Subjects/Plugins.md: -------------------------------------------------------------------------------- 1 | ## Introducing Plugins 2 | 3 | Plugins are whatever you want them to be. They can be extensions to the code, or they can be complete modifications of the code. They allow you to build smaller (or bigger) projects that require Klasa, while still maintaining everything that Klasa offers. 4 | 5 | An example of Klasa Plugins in work is [klasa-dashboard-hooks](https://github.com/dirigeants/klasa-dashboard-hooks). 6 | 7 | # To Get Started Using Plugins 8 | 9 | It's very easy to get started with using the new plugin system. 10 | 11 | Say we have our main app like so: 12 | 13 | ```javascript 14 | const { Client } = require('klasa'); 15 | const config = require('./config.json'); 16 | 17 | new Client(config).login(config.token); 18 | ``` 19 | 20 | If you wanted to use the klasa-dashboard-hooks plugin, you would insert the following code (assuming you installed klasa-dashboard-hooks): 21 | 22 | ```javascript 23 | const { Client } = require('klasa'); 24 | const config = require('./config.json'); 25 | 26 | Client.use(require('klasa-dashboard-hooks')); 27 | 28 | new Client(config).login(config.token); 29 | ``` 30 | 31 | The client will be created, and you will be able to use all of the features of the plugin inside your bot. 32 | 33 | You can have as many plugins as you want, and they will loaded in the same order that you added them in your main app. 34 | 35 | # To Get Started Making Plugins 36 | 37 | The only requirement for making a plugin is to make sure you export an unbound function as the plugin. Here's a small example of what a plugin could look like: 38 | 39 | ```javascript 40 | // index.js 41 | const { Client: { plugin } } = require('klasa'); 42 | module.exports = { 43 | // [plugin] must be typed exactly like this. 44 | [plugin]() { 45 | this.klasaIsCool = true; 46 | } 47 | }; 48 | ``` 49 | 50 | Accessing `this.client.klasaIsCool` from within your bot would be true here, assuming you followed steps above to insert the plugin into your code with the `use` method. 51 | 52 | Besides that, you can basically do anything with your code. Your plugin can extend Klasa code, modify it, or even remove it completely. 53 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingArguments.md: -------------------------------------------------------------------------------- 1 | Arguments are the resolvers used to convert strings padded by users, into fully resolved parameters that are passed into the command. New arguments are created in the `./arguments/` folder. 2 | 3 | ```javascript 4 | const { Argument } = require('klasa'); 5 | 6 | module.exports = class extends Argument { 7 | 8 | run(arg, possible, message) { 9 | // This is where you want to validate arg and return a resolved param or throw an error 10 | } 11 | 12 | }; 13 | ``` 14 | 15 | The run method in {@link Argument} takes 3 parameters: 16 | 17 | | Name | Type | Description | 18 | | ---------------- | -------------------- | -------------------------------------- | 19 | | **arg** | string | The parameter given to parse | 20 | | **possible** | {@link Possible} | The Possible instance that is running | 21 | | **message** | {@link KlasaMessage} | The message that triggered the command | 22 | 23 | ```javascript 24 | const { Argument } = require('klasa'); 25 | const REGEX_EMOJI = /^(?:?$/; 26 | 27 | module.exports = class extends Argument { 28 | 29 | run(arg, possible, message) { 30 | const results = REGEX_EMOJI.exec(arg); 31 | const emoji = results ? this.client.emojis.get(results[1]) : null; 32 | if (emoji) return emoji; 33 | throw message.language.get('RESOLVER_INVALID_EMOJI', possible.name); 34 | } 35 | 36 | }; 37 | ``` 38 | 39 | How does the new argument work? 40 | 41 | 1. Let's consider arg is `<:klasa:354702113147846666>`. The result of `exec`uting `REGEX_EMOJI` on that string gives an array-like object: `['<:klasa:354702113147846666>', '354702113147846666', index: 0, input: '<:klasa:354702113147846666>']`. 42 | 1. There are cases the argument does not match, in that case, `results` would be `null`. So we verify its existence and get the first grouping match: `(\d{17,19})`, which gets the **id** of the emoji, and as we see in the result, it is in the second index: `'354702113147846666'`, and we get the emoji with said id. 43 | 1. If `results` was null, `emoji` would be `null` too due to the ternary condition, but there is also the possibility of emoji being undefined: when the client does not have the Emoji instance cached or is in a guild the bot is not in. The case is, **if the emoji is valid and found, we should return it**. 44 | 1. Finally, the argument was required and/or looping/repeating, so we should throw an error. That error must be a string and you can use i18n to have localized errors. 45 | 46 | And now, you can use this type in a command! For example, the following: 47 | 48 | ```javascript 49 | const { Command } = require('klasa'); 50 | 51 | module.exports = class extends Command { 52 | 53 | constructor(...args) { 54 | super(...args, { 55 | description: 'Get the name of an emoji.', 56 | usage: '' 57 | }); 58 | } 59 | 60 | run(msg, [emoji]) { 61 | return msg.send(`The name of the emoji ${emoji} is: ${emoji.name}`); 62 | } 63 | 64 | }; 65 | ``` 66 | 67 | >**note:** An Emoji argument already comes included with Klasa. 68 | 69 | ## Examples 70 | 71 | You can take a look at the [included core Arguments](https://github.com/dirigeants/klasa/tree/{@branch}/src/arguments), or see some [prebuilt Arguments on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/arguments). 72 | 73 | ## Further reading 74 | 75 | - {@tutorial CreatingCommands} 76 | - {@tutorial CreatingEvents} 77 | - {@tutorial CreatingExtendables} 78 | - {@tutorial CreatingFinalizers} 79 | - {@tutorial CreatingInhibitors} 80 | - {@tutorial CreatingLanguages} 81 | - {@tutorial CreatingMonitors} 82 | - {@tutorial CreatingProviders} 83 | - {@tutorial CreatingSerializers} 84 | - {@tutorial CreatingSQLProviders} 85 | - {@tutorial CreatingTasks} 86 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingCommands.md: -------------------------------------------------------------------------------- 1 | New commands are created in the `./commands/` folder, where subfolders are the categories offered in the help command. For instance adding `./commands/Misc/test.js` will create a command named `test` in the `Misc` category. Subcategories can also be created by adding a second folder level. 2 | 3 | ```javascript 4 | const { Command } = require('klasa'); 5 | 6 | module.exports = class extends Command { 7 | 8 | constructor(...args) { 9 | super(...args, { 10 | name: 'yourCommandName', 11 | enabled: true, 12 | runIn: ['text', 'dm'], 13 | cooldown: 0, 14 | deletable: false, 15 | bucket: 1, 16 | aliases: [], 17 | guarded: false, 18 | nsfw: false, 19 | permissionLevel: 0, 20 | requiredPermissions: [], 21 | requiredSettings: [], 22 | subcommands: false, 23 | description: '', 24 | quotedStringSupport: false, 25 | usage: '', 26 | usageDelim: undefined, 27 | extendedHelp: 'No extended help available.' 28 | }); 29 | } 30 | 31 | async run(message, [...params]) { 32 | // This is where you place the code you want to run for your command 33 | } 34 | 35 | async init() { 36 | /* 37 | * You can optionally define this method which will be run when the bot starts 38 | * (after login, so discord data is available via this.client) 39 | */ 40 | } 41 | 42 | }; 43 | ``` 44 | 45 | ## Options 46 | 47 | {@typedef CommandOptions} 48 | 49 | > All commands are required to return an [Object Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) you can do that by adding the `async` keyword to the function, there's no need to change anything else. 50 | 51 | > All {@link CommandOptions command options} are optional, the code above shows all default values. You can delete any line with an optional value that matches the default value. 52 | 53 | >`[...params]` represents a variable number of arguments give when the command is run. The name of the arguments in the array (and their count) is determined by the `usage` property and its given arguments. 54 | 55 | ## Examples 56 | 57 | You can take a look at the [included core Commands](https://github.com/dirigeants/klasa/tree/{@branch}/src/commands), or see some [prebuilt Commands on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/commands). 58 | 59 | ## Further Reading: 60 | 61 | - {@tutorial CreatingArguments} 62 | - {@tutorial CreatingEvents} 63 | - {@tutorial CreatingExtendables} 64 | - {@tutorial CreatingFinalizers} 65 | - {@tutorial CreatingInhibitors} 66 | - {@tutorial CreatingLanguages} 67 | - {@tutorial CreatingMonitors} 68 | - {@tutorial CreatingProviders} 69 | - {@tutorial CreatingSerializers} 70 | - {@tutorial CreatingSQLProviders} 71 | - {@tutorial CreatingTasks} 72 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingEvents.md: -------------------------------------------------------------------------------- 1 | Events are placed in `./events/`. If a conflicting event is present in both the core and your client, *only your client version* is loaded and will run when that event is triggered. 2 | 3 | Their structure is the following: 4 | 5 | ```javascript 6 | const { Event } = require('klasa'); 7 | 8 | module.exports = class extends Event { 9 | 10 | constructor(...args) { 11 | super(...args, { 12 | name: 'yourEventName', 13 | enabled: true, 14 | event: 'theEventToListenTo', 15 | emitter: client, 16 | once: false 17 | }); 18 | } 19 | 20 | run(...params) { 21 | // This is where you place the code you want to run for your event 22 | } 23 | 24 | async init() { 25 | /* 26 | * You can optionally define this method which will be run when the bot starts 27 | * (after login, so discord data is available via this.client) 28 | */ 29 | } 30 | 31 | }; 32 | ``` 33 | 34 | Where `...params` are arguments you would *normally* get from those events. For example, while the `ready` event would only have none, the `guildMemberAdd` event would be `member`. 35 | 36 | ## Options 37 | 38 | {@typedef EventOptions} 39 | 40 | ## Examples 41 | 42 | You can take a look at the [included core Events](https://github.com/dirigeants/klasa/tree/{@branch}/src/events), or see some [prebuilt Events on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/events). 43 | 44 | ## Further Reading: 45 | 46 | - {@tutorial CreatingArguments} 47 | - {@tutorial CreatingCommands} 48 | - {@tutorial CreatingExtendables} 49 | - {@tutorial CreatingFinalizers} 50 | - {@tutorial CreatingInhibitors} 51 | - {@tutorial CreatingLanguages} 52 | - {@tutorial CreatingMonitors} 53 | - {@tutorial CreatingProviders} 54 | - {@tutorial CreatingSerializers} 55 | - {@tutorial CreatingSQLProviders} 56 | - {@tutorial CreatingTasks} 57 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingExtendables.md: -------------------------------------------------------------------------------- 1 | Extendables are pieces that extend objects or classes by copying its properties and methods like a blueprint. 2 | 3 | Extendables have the following syntax: 4 | 5 | 6 | 7 | ```javascript 8 | const { Extendable } = require('klasa'); 9 | 10 | class MyExtendable extends Extendable { 11 | 12 | constructor(...args) { 13 | super(...args, { 14 | appliesTo: [], 15 | name: 'nameOfExtendable', 16 | enabled: true 17 | }); 18 | } 19 | 20 | // Getters 21 | 22 | get myProperty() { 23 | // Make a getter 24 | } 25 | 26 | // Setters 27 | 28 | set myProperty(value) { 29 | // Make a setter 30 | } 31 | 32 | // Methods 33 | 34 | myMethod() { 35 | // Make a method 36 | } 37 | 38 | // Static Methods 39 | 40 | static myMethod() { 41 | // Make a static method 42 | } 43 | 44 | } 45 | 46 | // Static Properties 47 | 48 | MyExtendable.myStaticProperty = 'wew'; // Make a static property 49 | 50 | module.exports = MyExtendable; 51 | ``` 52 | 53 | 54 | 55 | ## Understanding extendable settings 56 | 57 | ```javascript 58 | const { Extendable } = require('klasa'); 59 | 60 | module.exports = class extends Extendable { 61 | 62 | constructor(...args) { 63 | super(...args, { 64 | appliesTo: [], 65 | name: 'nameOfExtendable', 66 | enabled: true 67 | }); 68 | } 69 | 70 | }; 71 | ``` 72 | 73 | {@typedef ExtendableOptions} 74 | 75 | ## Understanding extendables 76 | 77 | Understanding classes like a blueprint, and all its members (instance and static setters, getters, properties, and methods) as pieces of it, an Extendable would copy all the pieces into all the targetted structures with their respective names. You can define multiple members with different names inside the extended class. 78 | 79 | ## For Example 80 | 81 | Imagine we want to extend the [Message](https://discord.js.org/#/docs/main/master/class/Message) class 82 | so it has a method called `prompt` so you can do `Message#prompt("Are you sure you want to continue?")` 83 | everywhere in your code, resolving if the user confirms the prompt, or rejecting otherwise. Then, your 84 | extendable is likely to be like the following: 85 | 86 | > You can extend the Message object with this because you're likely to lock the prompt for a user in a channel, 87 | and Message has both properties of `author` and `channel`. 88 | 89 | ```js 90 | const { Extendable } = require('klasa'); 91 | const { Message } = require('@klasa/core'); 92 | const makePrompt = require('../lib/util/Prompt'); 93 | 94 | module.exports = class extends Extendable { 95 | 96 | constructor(...args) { 97 | super(...args, { appliesTo: [Message] }); 98 | } 99 | 100 | prompt() { 101 | // `this` is an instance of Message 102 | return makePrompt(this); 103 | } 104 | 105 | }; 106 | ``` 107 | 108 | After loading this extendable, `Message.prototype.prompt` will be available as a method that calls and returns `makePrompt`. 109 | 110 | ## Examples 111 | 112 | You can take a look at the [included core Extendables](https://github.com/dirigeants/klasa/tree/{@branch}/src/extendables), or see some [prebuilt Extendables on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/extendables). 113 | 114 | ## Further Reading: 115 | 116 | - {@tutorial CreatingArguments} 117 | - {@tutorial CreatingCommands} 118 | - {@tutorial CreatingEvents} 119 | - {@tutorial CreatingFinalizers} 120 | - {@tutorial CreatingInhibitors} 121 | - {@tutorial CreatingLanguages} 122 | - {@tutorial CreatingMonitors} 123 | - {@tutorial CreatingProviders} 124 | - {@tutorial CreatingSerializers} 125 | - {@tutorial CreatingSQLProviders} 126 | - {@tutorial CreatingTasks} 127 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingFinalizers.md: -------------------------------------------------------------------------------- 1 | Finalizers are functions run after successful commands, and this is the reason of why all commands **must** return an 2 | [Object Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise). 3 | 4 | Finalizers have the following syntax: 5 | 6 | ```javascript 7 | const { Finalizer } = require('klasa'); 8 | 9 | module.exports = class extends Finalizer { 10 | 11 | constructor(...args) { 12 | super(...args, { 13 | name: 'myFinalizerName', 14 | enabled: true 15 | }); 16 | } 17 | 18 | run(message, command, response, runTime) { 19 | // This is where you place the code you want to run for your finalizer 20 | } 21 | 22 | async init() { 23 | /* 24 | * You can optionally define this method which will be run when the bot starts 25 | * (after login, so discord data is available via this.client) 26 | */ 27 | } 28 | 29 | }; 30 | ``` 31 | 32 | ## Options 33 | 34 | {@typedef PieceOptions} 35 | 36 | ## Arguments: 37 | 38 | - **message**: The message object. 39 | - **command**: The command used (may not be the same as message.command). 40 | - **response**: The value the command returns. 41 | - **runTime**: The time it took to run the command. 42 | 43 | ## Examples 44 | 45 | You can take a look at the [included core Finalizers](https://github.com/dirigeants/klasa/tree/{@branch}/src/finalizers), or see some [prebuilt Finalizers on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/finalizers). 46 | 47 | ## Further Reading: 48 | 49 | - {@tutorial CreatingArguments} 50 | - {@tutorial CreatingCommands} 51 | - {@tutorial CreatingEvents} 52 | - {@tutorial CreatingExtendables} 53 | - {@tutorial CreatingInhibitors} 54 | - {@tutorial CreatingLanguages} 55 | - {@tutorial CreatingMonitors} 56 | - {@tutorial CreatingProviders} 57 | - {@tutorial CreatingSerializers} 58 | - {@tutorial CreatingSQLProviders} 59 | - {@tutorial CreatingTasks} 60 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingInhibitors.md: -------------------------------------------------------------------------------- 1 | Inhibitors are only ran on commands. They are used to check a variety of conditions before a 2 | command is ever ran, such as checking if a user has the right amount of permissions to use a 3 | command. Core inhibitors are loaded first, and if your code contains an inhibitor of the same name 4 | it overrides the core inhibitor. 5 | 6 | An inhibitor blocks a command by returning a truthy value (or a promise that fulfills with a 7 | truthy value): either a string (which is shown to the user) or `true` (for silent rejections). It 8 | doesn't matter whether you return or throw (or resolve or reject a returned promise); the value is 9 | treated the same. 10 | 11 | ```javascript 12 | const { Inhibitor } = require('klasa'); 13 | 14 | module.exports = class extends Inhibitor { 15 | 16 | constructor(...args) { 17 | super(...args, { 18 | name: 'myInhibitorName', 19 | enabled: true, 20 | spamProtection: false 21 | }); 22 | } 23 | 24 | async run(message, command) { 25 | // This is where you place the code you want to run for your inhibitor 26 | } 27 | 28 | async init() { 29 | /* 30 | * You can optionally define this method which will be run when the bot starts 31 | * (after login, so discord data is available via this.client) 32 | */ 33 | } 34 | 35 | }; 36 | 37 | ``` 38 | 39 | ## Options 40 | 41 | {@typedef InhibitorOptions} 42 | 43 | ## Examples 44 | 45 | You can take a look at the [included core Inhibitors](https://github.com/dirigeants/klasa/tree/{@branch}/src/inhibitors), or see some [prebuilt Inhibitors on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/inhibitors). 46 | 47 | ## Further Reading: 48 | 49 | - {@tutorial CreatingArguments} 50 | - {@tutorial CreatingCommands} 51 | - {@tutorial CreatingEvents} 52 | - {@tutorial CreatingExtendables} 53 | - {@tutorial CreatingFinalizers} 54 | - {@tutorial CreatingLanguages} 55 | - {@tutorial CreatingMonitors} 56 | - {@tutorial CreatingProviders} 57 | - {@tutorial CreatingSerializers} 58 | - {@tutorial CreatingSQLProviders} 59 | - {@tutorial CreatingTasks} 60 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingMonitors.md: -------------------------------------------------------------------------------- 1 | Monitors are special in that they will always run on any message. This is particularly 2 | useful when you need to do checking on the message, such as checking if a message 3 | contains a vulgar word (profanity filter). They are almost completely identical to 4 | inhibitors, the only difference between one is ran on the message, and the other 5 | is ran on the command. Monitors are loaded as core first, and if your code contains 6 | a monitor of the same name it overrides the core monitor. 7 | 8 | Their structure is identical to inhibitors, being the only difference is that you 9 | don't pass a command parameter to them. 10 | 11 | ```javascript 12 | const { Monitor } = require('klasa'); 13 | 14 | module.exports = class extends Monitor { 15 | 16 | constructor(...args) { 17 | super(...args, { 18 | name: 'yourMonitorName', 19 | enabled: true, 20 | ignoreBots: true, 21 | ignoreSelf: true, 22 | ignoreOthers: true, 23 | ignoreWebhooks: true, 24 | ignoreEdits: true, 25 | ignoreBlacklistedUsers: true, 26 | ignoreBlacklistedGuilds: true 27 | }); 28 | } 29 | 30 | run(message) { 31 | // This is where you place the code you want to run for your monitor 32 | } 33 | 34 | async init() { 35 | /* 36 | * You can optionally define this method which will be run when the bot starts 37 | * (after login, so discord data is available via this.client) 38 | */ 39 | } 40 | 41 | }; 42 | ``` 43 | 44 | ## Options 45 | 46 | {@typedef MonitorOptions} 47 | 48 | >As with all other pieces, you can omit any optional option that match the default values. 49 | 50 | ## Examples 51 | 52 | You can take a look at the [included core Monitors](https://github.com/dirigeants/klasa/tree/{@branch}/src/monitors), or see some [prebuilt Monitors on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/monitors). 53 | 54 | ## Further Reading: 55 | 56 | - {@tutorial CreatingArguments} 57 | - {@tutorial CreatingCommands} 58 | - {@tutorial CreatingEvents} 59 | - {@tutorial CreatingExtendables} 60 | - {@tutorial CreatingFinalizers} 61 | - {@tutorial CreatingInhibitors} 62 | - {@tutorial CreatingLanguages} 63 | - {@tutorial CreatingProviders} 64 | - {@tutorial CreatingSerializers} 65 | - {@tutorial CreatingSQLProviders} 66 | - {@tutorial CreatingTasks} 67 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingProviders.md: -------------------------------------------------------------------------------- 1 | Data Providers are special classes designed to make your life easier when you're 2 | using a **database**, there's **no** rule to make them. By default, Klasa uses 3 | JSON to store per-guild configuration. 4 | 5 | When you create a data provider, you can access to them by: `client.providers.get(ProviderName)`. 6 | 7 | ```javascript 8 | const { Provider } = require('klasa'); 9 | 10 | module.exports = class extends Provider { 11 | 12 | constructor(...args) { 13 | super(...args, { name: 'providerName' }); 14 | } 15 | 16 | init() { 17 | // The init method, usually checking file existence in file based 18 | // databases or connect to them. 19 | } 20 | 21 | /* Table methods */ 22 | 23 | hasTable(table) { 24 | // The code to check if a table exists 25 | } 26 | 27 | createTable(table) { 28 | // The code to create a table, in SQL databases, they take two 29 | // arguments. 30 | } 31 | 32 | deleteTable(table) { 33 | // The code to delete/drop a table. 34 | } 35 | 36 | /* Document methods */ 37 | 38 | getAll(table) { 39 | // Get all values from a table 40 | } 41 | 42 | getKeys(table) { 43 | // Get all keys (ids) from a table 44 | } 45 | 46 | get(table, entryID) { 47 | // Get an entry from a table 48 | } 49 | 50 | has(table, entryID) { 51 | // Check if the entry exists in a table 52 | } 53 | 54 | getRandom(table) { 55 | // Get a random key from the a table 56 | } 57 | 58 | create(table, entryID, data) { 59 | // Create a new entry to a table 60 | } 61 | 62 | set(...args) { 63 | // Reserved for retro-compatibility 64 | return this.create(...args); 65 | } 66 | 67 | insert(...args) { 68 | // Reserved for retro-compatibility 69 | return this.create(...args); 70 | } 71 | 72 | update(table, entryID, data) { 73 | // Update an entry from a table 74 | } 75 | 76 | replace(table, entryID, data) { 77 | // Perform a destructive write, where the previous data gets overwritten by the new one 78 | } 79 | 80 | delete(table, entryID) { 81 | // Delete an entry from a table 82 | } 83 | 84 | }; 85 | 86 | ``` 87 | 88 | The example above is the JSON provider used in klasa, and interfacing with the {@link SettingsGateway}. 89 | 90 | ## Options 91 | 92 | {@typedef PieceOptions} 93 | 94 | ## Accessing Providers 95 | 96 | The {@link ProviderStore providers} are stored in the main {@link KlasaClient} object, in the {@link KlasaClient#providers providers} property. This has an entry 97 | for each provider added, based on its `name`. So for example if you have it set as 98 | `postgresql` , you can access it through `client.providers.get('postgresql');`. 99 | 100 | ## Examples 101 | 102 | You can take a look at the [included core Providers](https://github.com/dirigeants/klasa/tree/{@branch}/src/providers), or see some [prebuilt Providers on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/providers). 103 | 104 | ## Further Reading: 105 | 106 | - {@tutorial CreatingArguments} 107 | - {@tutorial CreatingCommands} 108 | - {@tutorial CreatingEvents} 109 | - {@tutorial CreatingExtendables} 110 | - {@tutorial CreatingFinalizers} 111 | - {@tutorial CreatingInhibitors} 112 | - {@tutorial CreatingLanguages} 113 | - {@tutorial CreatingMonitors} 114 | - {@tutorial CreatingSerializers} 115 | - {@tutorial CreatingSQLProviders} 116 | - {@tutorial CreatingTasks} 117 | -------------------------------------------------------------------------------- /guides/Piece Basics/CreatingSerializers.md: -------------------------------------------------------------------------------- 1 | Serializers are pieces used for SettingsGateway's core to serialize and deserialize data. New serializers are created in the `./serializers/` folder. 2 | 3 | ```javascript 4 | const { Serializer } = require('klasa'); 5 | 6 | module.exports = class extends Serializer { 7 | 8 | constructor(...args) { 9 | super(...args, { aliases: [] }); 10 | } 11 | 12 | async deserialize(data, piece, language, guild) { 13 | // Code to resolve primitives into resolved data for the cache 14 | } 15 | 16 | serialize(value) { 17 | // Code to convert resolved data into primitives for database storage 18 | } 19 | 20 | stringify(value) { 21 | // Code to convert the value into a meaningful string. 22 | } 23 | 24 | }; 25 | ``` 26 | 27 | The deserialize method in {@link Serializer} takes 4 parameters: 28 | 29 | | Name | Type | Description | 30 | | ------------ | ------------------- | ----------------------------------------------------- | 31 | | **data** | any | The data to deserialize | 32 | | **piece** | {@link SchemaPiece} | The piece from the schema that called this serializer | 33 | | **language** | {@link Language} | The language instance for usage in translated errors | 34 | | **guild** | {@link Guild} | The guild instance passed in {@link Settings#update} | 35 | 36 | The serialize method takes 1 parameter: 37 | 38 | | Name | Type | Description | 39 | | --------- | --------- | ------------------------------------------- | 40 | | **value** | Primitive | A primitive value (string, number, boolean) | 41 | 42 | The stringify method takes 1 parameter: 43 | 44 | | Name | Type | Description | 45 | | --------- | --------- | ------------------------------------------- | 46 | | **value** | Primitive | A primitive value (string, number, boolean) | 47 | 48 | ## Examples 49 | 50 | You can take a look at the [included core Serializers](https://github.com/dirigeants/klasa/tree/{@branch}/src/serializers), or see some [prebuilt Serializers on klasa-pieces](https://github.com/dirigeants/klasa-pieces/tree/master/serializers). 51 | 52 | # Further reading 53 | 54 | - {@tutorial CreatingCommands} 55 | - {@tutorial CreatingEvents} 56 | - {@tutorial CreatingExtendables} 57 | - {@tutorial CreatingFinalizers} 58 | - {@tutorial CreatingInhibitors} 59 | - {@tutorial CreatingLanguages} 60 | - {@tutorial CreatingMonitors} 61 | - {@tutorial CreatingProviders} 62 | - {@tutorial CreatingSQLProviders} 63 | - {@tutorial CreatingTasks} 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klasa", 3 | "version": "0.6.0-dev", 4 | "description": "Klasa: Croatian for 'class', is a class based remix on Komada.", 5 | "homepage": "https://klasa.js.org/", 6 | "bugs": { 7 | "url": "https://github.com/dirigeants/klasa/issues" 8 | }, 9 | "license": "MIT", 10 | "author": "BDistin", 11 | "main": "dist/src/index.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/dirigeants/klasa.git" 15 | }, 16 | "scripts": { 17 | "prepublishOnly": "yarn build", 18 | "build": "tsc", 19 | "test": "ava --timeout=2m", 20 | "test:lint": "eslint --ext ts src test", 21 | "test:coverage": "npx nyc check-coverage --lines 0 --functions 0 --branches 0", 22 | "coverage": "npx nyc --require source-map-support/register npm test", 23 | "coverage:report": "npx nyc report --reporter=html", 24 | "lint": "eslint --fix --ext ts src test", 25 | "docs": "typedoc", 26 | "docs:html": "typedoc --inputFiles src --mode file --out docs" 27 | }, 28 | "dependencies": { 29 | "@klasa/cache": "^0.0.3", 30 | "@klasa/console": "^0.0.3", 31 | "@klasa/cron": "^0.0.1", 32 | "@klasa/duration": "^0.0.3", 33 | "@klasa/ratelimits": "^0.1.0", 34 | "@klasa/request-handler": "^0.0.3", 35 | "@klasa/stopwatch": "^0.0.1", 36 | "@klasa/timer-manager": "0.0.1", 37 | "@klasa/timestamp": "^0.0.1", 38 | "@klasa/type": "^0.0.1", 39 | "@klasa/utils": "^0.1.0", 40 | "discord-md-tags": "1.0.0", 41 | "fs-nextra": "^0.5.1" 42 | }, 43 | "peerDependencies": { 44 | "@klasa/core": "^0.0.3", 45 | "@klasa/dapi-types": "^0.2.2" 46 | }, 47 | "devDependencies": { 48 | "@ava/typescript": "^1.1.1", 49 | "@klasa/core": "dirigeants/core#build", 50 | "@klasa/dapi-types": "^0.2.2", 51 | "@types/node": "14", 52 | "@types/node-fetch": "^2.5.7", 53 | "@types/ws": "^7.2.6", 54 | "@typescript-eslint/eslint-plugin": "^3.10.1", 55 | "@typescript-eslint/parser": "^3.10.1", 56 | "ava": "^3.12.1", 57 | "eslint": "^7.7.0", 58 | "eslint-config-klasa": "dirigeants/klasa-lint", 59 | "nyc": "^15.1.0", 60 | "source-map-support": "^0.5.19", 61 | "typedoc": "^0.18.0", 62 | "typescript": "^4.0.2" 63 | }, 64 | "engines": { 65 | "node": ">=12.6.0" 66 | }, 67 | "ava": { 68 | "files": [ 69 | "test/**/*.ts", 70 | "!test/lib" 71 | ], 72 | "typescript": { 73 | "extensions": [ 74 | "ts" 75 | ], 76 | "rewritePaths": { 77 | "test/": "dist/test/" 78 | } 79 | } 80 | }, 81 | "files": [ 82 | "dist/src" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /src/arguments/argument.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Argument { 8 | const entry = this.client.arguments.get(argument); 9 | if (entry) return entry; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'argument'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/arguments.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...argument'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('argument') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/boolean.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | const truths = ['1', 'true', '+', 't', 'yes', 'y']; 6 | const falses = ['0', 'false', '-', 'f', 'no', 'n']; 7 | 8 | export default class CoreArgument extends Argument { 9 | 10 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 11 | super(store, directory, file, { aliases: ['bool'] }); 12 | } 13 | 14 | public run(argument: string, possible: Possible, message: Message): boolean { 15 | const boolean = String(argument).toLowerCase(); 16 | if (truths.includes(boolean)) return true; 17 | if (falses.includes(boolean)) return false; 18 | throw message.language.get('RESOLVER_INVALID_BOOL', possible.name); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/arguments/channel.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Channel, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public async run(argument: string, possible: Possible, message: Message): Promise { 8 | // Regular Channel support 9 | const channelID = Argument.regex.channel.exec(argument); 10 | const channel = channelID ? await this.client.channels.fetch(channelID[1]).catch(() => null) : null; 11 | if (channel) return channel; 12 | 13 | // DM Channel support 14 | const userID = Argument.regex.userOrMember.exec(argument); 15 | const user = userID ? await this.client.users.fetch(userID[1]).catch(() => null) : null; 16 | if (user) return user.openDM(); 17 | throw message.language.get('RESOLVER_INVALID_CHANNEL', possible.name); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/arguments/channels.ts: -------------------------------------------------------------------------------- 1 | import { Argument, MultiArgument, ArgumentStore } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...channel'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('channel') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/command.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Command, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['cmd'] }); 9 | } 10 | 11 | public run(argument: string, possible: Possible, message: Message): Command { 12 | const command = this.client.commands.get(argument.toLowerCase()); 13 | if (command) return command; 14 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'command'); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/arguments/commands.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...command'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('command') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/custom.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible, CustomUsageArgument } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public async run(argument: string, possible: Possible, message: Message, custom: CustomUsageArgument): Promise { 8 | try { 9 | return await custom(argument, possible, message, message.params); 10 | } catch (err) { 11 | if (err) throw err; 12 | throw message.language.get('RESOLVER_INVALID_CUSTOM', possible.name, possible.type); 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/arguments/date.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Date { 8 | const date = new Date(argument); 9 | if (!isNaN(date.getTime()) && date.getTime() > Date.now()) return date; 10 | throw message.language.get('RESOLVER_INVALID_DATE', possible.name); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/default.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): string { 8 | const literal = possible.name.toLowerCase(); 9 | if (typeof argument === 'undefined' || argument.toLowerCase() !== literal) message.args.splice(message.params.length, 0, undefined); 10 | return literal; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/dmChannel.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { DMChannel, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public async run(argument: string, possible: Possible, message: Message): Promise { 8 | const userID = Argument.regex.userOrMember.exec(argument); 9 | const user = userID ? await this.client.users.fetch(userID[1]).catch(() => null) : null; 10 | if (user) return user.openDM(); 11 | throw message.language.get('RESOLVER_INVALID_CHANNEL', possible.name); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/arguments/dmChannels.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...dmChannel'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('dmChannel') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/duration.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | import { Duration } from '@klasa/duration'; 3 | 4 | import type { Message } from '@klasa/core'; 5 | 6 | export default class CoreArgument extends Argument { 7 | 8 | public run(argument: string, possible: Possible, message: Message): Date { 9 | const date = new Duration(argument).fromNow; 10 | if (!isNaN(date.getTime()) && date.getTime() > Date.now()) return date; 11 | throw message.language.get('RESOLVER_INVALID_DURATION', possible.name); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/arguments/emoji.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { GuildEmoji, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): GuildEmoji { 8 | const emojiID = Argument.regex.emoji.exec(argument); 9 | const emoji = emojiID ? this.client.emojis.get(emojiID[1]) : null; 10 | if (emoji) return emoji; 11 | throw message.language.get('RESOLVER_INVALID_EMOJI', possible.name); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/arguments/emojis.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...emoji'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('emoji') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/event.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Event, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Event { 8 | const event = this.client.events.get(argument); 9 | if (event) return event; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'event'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/events.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...event'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('event') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/extendable.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible, Extendable } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Extendable { 8 | const extendable = this.client.extendables.get(argument); 9 | if (extendable) return extendable; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'extendable'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/extendables.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...extendable'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('extendable') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/finalizer.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Finalizer, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Finalizer { 8 | const finalizer = this.client.finalizers.get(argument); 9 | if (finalizer) return finalizer; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'finalizer'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/finalizers.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...finalizer'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('finalizer') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/float.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['num', 'number'] }); 9 | } 10 | 11 | public run(argument: string, possible: Possible, message: Message): number | null { 12 | const { min, max } = possible; 13 | const number = parseFloat(argument); 14 | if (Number.isNaN(number)) throw message.language.get('RESOLVER_INVALID_FLOAT', possible.name); 15 | return Argument.minOrMax(this.client, number, min, max, possible, message) ? number : null; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/arguments/guild.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Guild, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Guild { 8 | const guild = Argument.regex.snowflake.test(argument) ? this.client.guilds.get(argument) : null; 9 | if (guild) return guild; 10 | throw message.language.get('RESOLVER_INVALID_GUILD', possible.name); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/guilds.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...guild'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('guild') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/hyperlink.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'url'; 2 | import { Argument, ArgumentStore, Possible } from 'klasa'; 3 | 4 | import type { Message } from '@klasa/core'; 5 | 6 | export default class CoreArgument extends Argument { 7 | 8 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 9 | super(store, directory, file, { aliases: ['url'] }); 10 | } 11 | 12 | public run(argument: string, possible: Possible, message: Message): string { 13 | const res = parse(argument); 14 | const hyperlink = res.protocol && res.hostname ? argument : null; 15 | if (hyperlink !== null) return hyperlink; 16 | throw message.language.get('RESOLVER_INVALID_URL', possible.name); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/arguments/inhibitor.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible, Inhibitor } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Inhibitor { 8 | const inhibitor = this.client.inhibitors.get(argument); 9 | if (inhibitor) return inhibitor; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'inhibitor'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/inhibitors.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...inhibitor'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('inhibitor') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/integer.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['int'] }); 9 | } 10 | 11 | public run(argument: string, possible: Possible, message: Message): number | null { 12 | const { min, max } = possible; 13 | const number = parseInt(argument); 14 | if (!Number.isInteger(number)) throw message.language.get('RESOLVER_INVALID_INT', possible.name); 15 | return Argument.minOrMax(this.client, number, min, max, possible, message) ? number : null; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/arguments/language.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Language, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Language { 8 | const language = this.client.languages.get(argument); 9 | if (language) return language; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'language'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/languages.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...language'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('language') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/literal.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): string { 8 | if (argument.toLowerCase() === possible.name.toLowerCase()) return possible.name; 9 | throw message.language.get('RESOLVER_INVALID_LITERAL', possible.name); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/arguments/member.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { GuildMember, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public async run(argument: string, possible: Possible, message: Message): Promise { 8 | const memberID = Argument.regex.userOrMember.exec(argument); 9 | const member = memberID ? await message.guild?.members.fetch(memberID[1]).catch(() => null) : null; 10 | if (member) return member; 11 | throw message.language.get('RESOLVER_INVALID_MEMBER', possible.name); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/arguments/members.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...member'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('member') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/message.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['msg'] }); 9 | } 10 | 11 | public async run(argument: string, possible: Possible, message: Message): Promise { 12 | const msg = Argument.regex.snowflake.test(argument) ? await message.channel.messages.fetch(argument).catch(() => null) : undefined; 13 | if (msg) return msg; 14 | throw message.language.get('RESOLVER_INVALID_MESSAGE', possible.name); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/arguments/messages.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...message'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('message') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/monitor.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible, Monitor } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Monitor { 8 | const monitor = this.client.monitors.get(argument); 9 | if (monitor) return monitor; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'monitor'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/monitors.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...monitor'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('monitor') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/piece.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Piece, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Piece { 8 | for (const store of this.client.pieceStores.values()) { 9 | const piece = store.get(argument); 10 | if (piece) return piece; 11 | } 12 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'piece'); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/arguments/pieces.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...piece'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('piece') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/provider.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible, Provider } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Provider { 8 | const provider = this.client.providers.get(argument); 9 | if (provider) return provider; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'provider'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/providers.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...provider'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('provider') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/regexp.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['reg', 'regex'] }); 9 | } 10 | 11 | public run(argument: string, possible: Possible, message: Message): RegExpExecArray { 12 | const regex = possible.regex as RegExp; 13 | const results = regex.exec(argument); 14 | if (results) return results; 15 | throw message.language.get('RESOLVER_INVALID_REGEX_MATCH', possible.name, regex.toString()); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/arguments/restString.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Possible, CustomUsageArgument } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { name: '...string', aliases: ['...str'] }); 9 | } 10 | 11 | public get base(): Argument { 12 | return this.store.get('string') as Argument; 13 | } 14 | 15 | public run(argument: string, possible: Possible, message: Message, custom: CustomUsageArgument): string { 16 | if (!argument) throw message.language.get('RESOLVER_INVALID_STRING', possible.name); 17 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 18 | const { args, usage: { usageDelim } } = message.prompter!; 19 | const index = args.indexOf(argument); 20 | const rest = args.splice(index, args.length - index).join(usageDelim); 21 | args.push(rest); 22 | return this.base.run(rest, possible, message, custom) as string; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/arguments/role.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Role, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Role { 8 | const roleID = Argument.regex.role.exec(argument); 9 | const role = roleID ? message.guild?.roles.get(roleID[1]) : null; 10 | if (role) return role; 11 | throw message.language.get('RESOLVER_INVALID_ROLE', possible.name); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/arguments/roles.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...role'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('role') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/store.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | 3 | import type { Store, Piece, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Store { 8 | const store = this.client.pieceStores.get(argument); 9 | if (store) return store; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'store'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/stores.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...store'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('store') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/string.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Possible } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['str'] }); 9 | } 10 | 11 | public run(argument: string, possible: Possible, message: Message): string | null { 12 | if (!argument) throw message.language.get('RESOLVER_INVALID_STRING', possible.name); 13 | const { min, max } = possible; 14 | return Argument.minOrMax(this.client, argument.length, min, max, possible, message, 'RESOLVER_STRING_SUFFIX') ? argument : null; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/arguments/task.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible, Task } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public run(argument: string, possible: Possible, message: Message): Task { 8 | const task = this.client.tasks.get(argument); 9 | if (task) return task; 10 | throw message.language.get('RESOLVER_INVALID_PIECE', possible.name, 'task'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/tasks.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...task'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('task') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/textChannel.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | import { ChannelType } from '@klasa/dapi-types'; 3 | 4 | import type { TextChannel, Message } from '@klasa/core'; 5 | 6 | export default class CoreArgument extends Argument { 7 | 8 | public async run(argument: string, possible: Possible, message: Message): Promise { 9 | const channelID = Argument.regex.channel.exec(argument); 10 | const channel = channelID ? await this.client.channels.fetch(channelID[1]).catch(() => null) : null; 11 | if (channel && channel.type === ChannelType.GuildText) return channel as TextChannel; 12 | throw message.language.get('RESOLVER_INVALID_CHANNEL', possible.name); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/arguments/textChannels.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...textChannel'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('textChannel') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/time.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible, CustomUsageArgument } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public get date(): Argument { 8 | return this.store.get('date') as Argument; 9 | } 10 | 11 | public get duration(): Argument { 12 | return this.store.get('duration') as Argument; 13 | } 14 | 15 | public run(argument: string, possible: Possible, message: Message, custom: CustomUsageArgument): Date { 16 | let date: Date | undefined; 17 | try { 18 | date = this.date.run(argument, possible, message, custom) as Date; 19 | } catch (err) { 20 | try { 21 | date = this.duration.run(argument, possible, message, custom) as Date; 22 | } catch (error) { 23 | // noop 24 | } 25 | } 26 | if (date && !Number.isNaN(date.getTime()) && date.getTime() > Date.now()) return date; 27 | throw message.language.get('RESOLVER_INVALID_TIME', possible.name); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/arguments/user.ts: -------------------------------------------------------------------------------- 1 | import { Argument, ArgumentStore, Possible } from 'klasa'; 2 | 3 | import type { User, Message } from '@klasa/core'; 4 | 5 | export default class CoreArgument extends Argument { 6 | 7 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['mention'] }); 9 | } 10 | 11 | public async run(argument: string, possible: Possible, message: Message): Promise { 12 | const userID = Argument.regex.userOrMember.exec(argument); 13 | const user = userID ? await this.client.users.fetch(userID[1]).catch(() => null) : null; 14 | if (user) return user; 15 | throw message.language.get('RESOLVER_INVALID_USER', possible.name); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/arguments/users.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...user'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('user') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/arguments/voiceChannel.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Possible } from 'klasa'; 2 | import { ChannelType } from '@klasa/dapi-types'; 3 | 4 | import type { VoiceChannel, Message } from '@klasa/core'; 5 | 6 | export default class CoreArgument extends Argument { 7 | 8 | public async run(argument: string, possible: Possible, message: Message): Promise { 9 | const channelID = Argument.regex.channel.exec(argument); 10 | const channel = channelID ? await this.client.channels.fetch(channelID[1]).catch(() => null) : null; 11 | if (channel && channel.type === ChannelType.GuildVoice) return channel as VoiceChannel; 12 | throw message.language.get('RESOLVER_INVALID_CHANNEL', possible.name); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/arguments/voiceChannels.ts: -------------------------------------------------------------------------------- 1 | import { MultiArgument, ArgumentStore, Argument } from 'klasa'; 2 | 3 | export default class CoreMultiArgument extends MultiArgument { 4 | 5 | public constructor(store: ArgumentStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['...voiceChannel'] }); 7 | } 8 | 9 | public get base(): Argument { 10 | return this.store.get('voiceChannel') as Argument; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/Admin/disable.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | import { codeblock } from 'discord-md-tags'; 3 | 4 | import type { Message, Piece } from '@klasa/core'; 5 | 6 | export default class extends Command { 7 | 8 | constructor(store: CommandStore, directory: string, files: readonly string[]) { 9 | super(store, directory, files, { 10 | permissionLevel: 10, 11 | guarded: true, 12 | description: language => language.get('COMMAND_DISABLE_DESCRIPTION'), 13 | usage: '' 14 | }); 15 | } 16 | 17 | public async run(message: Message, [piece]: Piece[]): Promise { 18 | if ((piece.type === 'event' && piece.name === 'coreMessage') || 19 | (piece.type === 'monitor' && piece.name === 'commandHandler') || 20 | (piece.type === 'action' && piece.name === 'MESSAGE_CREATE')) { 21 | return message.replyLocale('COMMAND_DISABLE_WARN'); 22 | } 23 | piece.disable(); 24 | return message.reply(mb => mb.setContent(codeblock('diff') `${message.language.get('COMMAND_DISABLE', [piece.type, piece.name])}`)); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/Admin/enable.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | import { codeblock } from 'discord-md-tags'; 3 | 4 | import type { Message, Piece } from '@klasa/core'; 5 | 6 | export default class extends Command { 7 | 8 | constructor(store: CommandStore, directory: string, files: readonly string[]) { 9 | super(store, directory, files, { 10 | permissionLevel: 10, 11 | guarded: true, 12 | description: language => language.get('COMMAND_ENABLE_DESCRIPTION'), 13 | usage: '' 14 | }); 15 | } 16 | 17 | public async run(message: Message, [piece]: Piece[]): Promise { 18 | piece.enable(); 19 | return message.reply(mb => mb.setContent(codeblock('diff') `${message.language.get('COMMAND_ENABLE', [piece.type, piece.name])}`)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/Admin/eval.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | import { inspect } from 'util'; 3 | import { Stopwatch } from '@klasa/stopwatch'; 4 | import { codeblock } from 'discord-md-tags'; 5 | import { Type } from '@klasa/type'; 6 | import { isThenable } from '@klasa/utils'; 7 | 8 | import type { Message } from '@klasa/core'; 9 | 10 | export default class extends Command { 11 | 12 | public constructor(store: CommandStore, directory: string, files: readonly string[]) { 13 | super(store, directory, files, { 14 | aliases: ['ev'], 15 | permissionLevel: 10, 16 | guarded: true, 17 | description: language => language.get('COMMAND_EVAL_DESCRIPTION'), 18 | extendedHelp: language => language.get('COMMAND_EVAL_EXTENDEDHELP'), 19 | usage: '' 20 | }); 21 | } 22 | 23 | public async run(message: Message, [code]: string[]): Promise { 24 | const { success, result, time, type } = await this.eval(message, code); 25 | const footer = codeblock('ts')`${type}`; 26 | const output = message.language.get(success ? 'COMMAND_EVAL_OUTPUT' : 'COMMAND_EVAL_ERROR', 27 | time, codeblock('js')`${result}`, footer); 28 | 29 | if ('silent' in message.flagArgs) return []; 30 | 31 | // Handle too-long-messages 32 | if (output.length > 2000) { 33 | if (message.guild && message.channel.attachable) { 34 | return message.channel.send(mb => mb 35 | .setContent(message.language.get('COMMAND_EVAL_SENDFILE', time, footer)) 36 | .addFile({ file: Buffer.from(result), name: 'output.txt' })); 37 | } 38 | this.client.emit('log', result); 39 | return message.replyLocale('COMMAND_EVAL_SENDCONSOLE', [time, footer]); 40 | } 41 | 42 | // If it's a message that can be sent correctly, send it 43 | return message.reply(mb => mb.setContent(output)); 44 | } 45 | 46 | // Eval the input 47 | private async eval(message: Message, code: string): Promise { 48 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 49 | // @ts-expect-error 50 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 51 | const msg = message; 52 | const { flagArgs: flags } = message; 53 | code = code.replace(/[“”]/g, '"').replace(/[‘’]/g, "'"); 54 | const stopwatch = new Stopwatch(); 55 | let success, syncTime, asyncTime, result; 56 | let thenable = false; 57 | let type; 58 | try { 59 | if (flags.async) code = `(async () => {\n${code}\n})();`; 60 | result = eval(code); 61 | syncTime = stopwatch.toString(); 62 | type = new Type(result); 63 | if (isThenable(result)) { 64 | thenable = true; 65 | stopwatch.restart(); 66 | result = await result; 67 | asyncTime = stopwatch.toString(); 68 | } 69 | success = true; 70 | } catch (error) { 71 | if (!syncTime) syncTime = stopwatch.toString(); 72 | if (!type) type = new Type(error); 73 | if (thenable && !asyncTime) asyncTime = stopwatch.toString(); 74 | if (error && error.stack) this.client.emit('error', error.stack); 75 | result = error; 76 | success = false; 77 | } 78 | 79 | stopwatch.stop(); 80 | if (typeof result !== 'string') { 81 | result = inspect(result, { 82 | depth: flags.depth ? parseInt(flags.depth) || 0 : 0, 83 | showHidden: Boolean(flags.showHidden) 84 | }); 85 | } 86 | return { success, type, time: this.formatTime(syncTime, asyncTime), result }; 87 | } 88 | 89 | public formatTime(syncTime: string, asyncTime: string | undefined): string { 90 | return asyncTime ? `⏱ ${asyncTime}<${syncTime}>` : `⏱ ${syncTime}`; 91 | } 92 | 93 | } 94 | 95 | interface EvalResults { 96 | success: boolean; 97 | type: Type; 98 | time: string; 99 | result: string; 100 | } 101 | -------------------------------------------------------------------------------- /src/commands/Admin/load.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | import { Stopwatch } from '@klasa/stopwatch'; 3 | import { pathExists } from 'fs-nextra'; 4 | import { join } from 'path'; 5 | 6 | import type { Message, Store, Piece } from '@klasa/core'; 7 | 8 | export default class extends Command { 9 | 10 | private readonly regExp = /\\\\?|\//g; 11 | 12 | constructor(store: CommandStore, directory: string, files: readonly string[]) { 13 | super(store, directory, files, { 14 | aliases: ['l'], 15 | permissionLevel: 10, 16 | guarded: true, 17 | description: language => language.get('COMMAND_LOAD_DESCRIPTION'), 18 | usage: '[core] ', 19 | usageDelim: ' ' 20 | }); 21 | } 22 | 23 | async run(message: Message, [core, store, rawPath]: [string, Store, string]): Promise { 24 | const path = (rawPath.endsWith('.js') ? rawPath : `${rawPath}.js`).split(this.regExp); 25 | const timer = new Stopwatch(); 26 | const piece = await (core ? this.tryEach(store, path) : store.load(store.userDirectory, path)); 27 | 28 | try { 29 | if (!piece) throw message.language.get('COMMAND_LOAD_FAIL'); 30 | await piece.init(); 31 | return message.replyLocale('COMMAND_LOAD', [timer.stop(), store.name, piece.name]); 32 | } catch (error) { 33 | timer.stop(); 34 | throw message.language.get('COMMAND_LOAD_ERROR', store.name, piece ? piece.name : path.join('/'), error); 35 | } 36 | } 37 | 38 | private async tryEach(store: Store, path: readonly string[]) { 39 | // eslint-disable-next-line dot-notation 40 | for (const dir of store['coreDirectories']) if (await pathExists(join(dir, ...path))) return store.load(dir, path); 41 | return undefined; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/Admin/reboot.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class extends Command { 6 | 7 | constructor(store: CommandStore, directory: string, files: readonly string[]) { 8 | super(store, directory, files, { 9 | permissionLevel: 10, 10 | guarded: true, 11 | description: language => language.get('COMMAND_REBOOT_DESCRIPTION') 12 | }); 13 | } 14 | 15 | public async run(message: Message): Promise { 16 | await message.replyLocale('COMMAND_REBOOT').catch(err => this.client.emit('error', err)); 17 | await Promise.all(this.client.providers.map(provider => provider.shutdown())); 18 | process.exit(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/Admin/reload.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | import { Piece, Store, Message } from '@klasa/core'; 3 | import { Stopwatch } from '@klasa/stopwatch'; 4 | 5 | export default class extends Command { 6 | 7 | constructor(store: CommandStore, directory: string, files: readonly string[]) { 8 | super(store, directory, files, { 9 | aliases: ['r'], 10 | permissionLevel: 10, 11 | guarded: true, 12 | description: language => language.get('COMMAND_RELOAD_DESCRIPTION'), 13 | usage: '' 14 | }); 15 | } 16 | 17 | public async run(message: Message, [piece]: [Piece | Store | 'everything']): Promise { 18 | if (piece === 'everything') return this.everything(message); 19 | if (piece instanceof Store) { 20 | const timer = new Stopwatch(); 21 | await piece.loadAll(); 22 | await piece.init(); 23 | return message.replyLocale('COMMAND_RELOAD_ALL', [piece, timer.stop()]); 24 | } 25 | 26 | try { 27 | const item = await piece.reload(); 28 | if (!item) throw new Error('Failed to reload.'); 29 | 30 | const timer = new Stopwatch(); 31 | return message.replyLocale('COMMAND_RELOAD', [item.type, item.name, timer.stop()]); 32 | } catch (err) { 33 | piece.store.add(piece); 34 | return message.replyLocale('COMMAND_RELOAD_FAILED', [piece.type, piece.name]); 35 | } 36 | } 37 | 38 | public async everything(message: Message): Promise { 39 | const timer = new Stopwatch(); 40 | await Promise.all(this.client.pieceStores.map(async (store) => { 41 | await store.loadAll(); 42 | await store.init(); 43 | })); 44 | return message.replyLocale('COMMAND_RELOAD_EVERYTHING', [timer.stop()]); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/Admin/transfer.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | import { copy } from 'fs-nextra'; 3 | import { promises as fsp } from 'fs'; 4 | import { resolve, join } from 'path'; 5 | 6 | import type { Piece, Message } from '@klasa/core'; 7 | 8 | export default class extends Command { 9 | 10 | constructor(store: CommandStore, directory: string, files: readonly string[]) { 11 | super(store, directory, files, { 12 | permissionLevel: 10, 13 | guarded: true, 14 | description: language => language.get('COMMAND_TRANSFER_DESCRIPTION'), 15 | usage: '' 16 | }); 17 | } 18 | 19 | public async run(message: Message, [piece]: [Piece]): Promise { 20 | const file = join(...piece.file); 21 | const fileLocation = resolve(piece.directory, file); 22 | await fsp.access(fileLocation).catch(() => { throw message.language.get('COMMAND_TRANSFER_ERROR'); }); 23 | try { 24 | await copy(fileLocation, join(piece.store.userDirectory, file)); 25 | piece.store.load(piece.store.userDirectory, piece.file); 26 | return message.replyLocale('COMMAND_TRANSFER_SUCCESS', [piece.type, piece.name]); 27 | } catch (err) { 28 | this.client.emit('error', err.stack); 29 | return message.replyLocale('COMMAND_TRANSFER_FAILED', [piece.type, piece.name]); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/Admin/unload.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | 3 | import type { Message, Piece } from '@klasa/core'; 4 | 5 | export default class extends Command { 6 | 7 | constructor(store: CommandStore, directory: string, files: readonly string[]) { 8 | super(store, directory, files, { 9 | aliases: ['u'], 10 | permissionLevel: 10, 11 | guarded: true, 12 | description: language => language.get('COMMAND_UNLOAD_DESCRIPTION'), 13 | usage: '' 14 | }); 15 | } 16 | 17 | public async run(message: Message, [piece]: [Piece]): Promise { 18 | if ((piece.type === 'event' && piece.name === 'message') || (piece.type === 'monitor' && piece.name === 'commandHandler')) { 19 | return message.replyLocale('COMMAND_UNLOAD_WARN'); 20 | } 21 | piece.unload(); 22 | return message.replyLocale('COMMAND_UNLOAD', [piece.type, piece.name]); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/General/Chat Bot Info/help.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | import { isFunction } from '@klasa/utils'; 3 | import { codeblock } from 'discord-md-tags'; 4 | import { ChannelType } from '@klasa/dapi-types'; 5 | 6 | import type { Message } from '@klasa/core'; 7 | 8 | export default class extends Command { 9 | 10 | constructor(store: CommandStore, directory: string, files: string[]) { 11 | super(store, directory, files, { 12 | aliases: ['commands'], 13 | guarded: true, 14 | description: language => language.get('COMMAND_HELP_DESCRIPTION'), 15 | usage: '(Command:command)' 16 | }); 17 | 18 | this.createCustomResolver('command', (arg, possible, message) => { 19 | if (!arg) return undefined; 20 | return this.client.arguments.get('command')?.run(arg, possible, message); 21 | }); 22 | } 23 | 24 | async run(message: Message, [command]: [Command]): Promise { 25 | if (command) { 26 | const info = [ 27 | `= ${command.name} = `, 28 | isFunction(command.description) ? command.description(message.language) : command.description, 29 | message.language.get('COMMAND_HELP_USAGE', command.usage.fullUsage(message)), 30 | message.language.get('COMMAND_HELP_EXTENDED'), 31 | isFunction(command.extendedHelp) ? command.extendedHelp(message.language) : command.extendedHelp 32 | ].join('\n'); 33 | return message.reply(mb => mb.setContent(codeblock('asciidoc') `${info}`)); 34 | } 35 | const help = await this.buildHelp(message); 36 | const categories = Object.keys(help); 37 | const helpMessage: string[] = []; 38 | for (let cat = 0; cat < categories.length; cat++) { 39 | helpMessage.push(`**${categories[cat]} Commands**:`, '```asciidoc'); 40 | const subCategories = Object.keys(help[categories[cat]]); 41 | for (let subCat = 0; subCat < subCategories.length; subCat++) helpMessage.push(`= ${subCategories[subCat]} =`, `${help[categories[cat]][subCategories[subCat]].join('\n')}\n`); 42 | helpMessage.push('```', '\u200b'); 43 | } 44 | 45 | const dm = await message.author.openDM(); 46 | 47 | let response: Message[] = []; 48 | 49 | try { 50 | response = await dm.send(mb => mb.setContent(helpMessage.join('\n')), { char: '\u200b' }); 51 | } catch { 52 | if (message.channel.type !== ChannelType.DM) await message.replyLocale('COMMAND_HELP_NODM'); 53 | } 54 | 55 | if (message.channel.type !== ChannelType.DM) await message.replyLocale('COMMAND_HELP_DM'); 56 | 57 | return response; 58 | } 59 | 60 | private async buildHelp(message: Message): Promise>> { 61 | const help: Record> = {}; 62 | 63 | const prefix = message.guildSettings.get('prefix'); 64 | const commandNames = [...this.client.commands.keys()]; 65 | const longest = commandNames.reduce((long, str) => Math.max(long, str.length), 0); 66 | 67 | await Promise.all(this.client.commands.map((command) => 68 | this.client.inhibitors.run(message, command, true) 69 | .then(() => { 70 | if (!Reflect.has(help, command.category)) help[command.category] = {}; 71 | if (!Reflect.has(help[command.category], command.subCategory)) Reflect.set(help[command.category], command.subCategory, []); 72 | const description = typeof command.description === 'function' ? command.description(message.language) : command.description; 73 | help[command.category][command.subCategory].push(`${prefix}${command.name.padEnd(longest)} :: ${description}`); 74 | }) 75 | .catch(() => { 76 | // noop 77 | }) 78 | )); 79 | 80 | return help; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/General/Chat Bot Info/info.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class extends Command { 6 | 7 | constructor(store: CommandStore, directory: string, files: string[]) { 8 | super(store, directory, files, { 9 | aliases: ['details', 'what'], 10 | guarded: true, 11 | description: language => language.get('COMMAND_INFO_DESCRIPTION') 12 | }); 13 | } 14 | 15 | public async run(message: Message): Promise<|Message[]> { 16 | return message.replyLocale('COMMAND_INFO'); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/General/Chat Bot Info/invite.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class extends Command { 6 | 7 | constructor(store: CommandStore, directory: string, files: string[]) { 8 | super(store, directory, files, { 9 | guarded: true, 10 | description: language => language.get('COMMAND_INVITE_DESCRIPTION') 11 | }); 12 | } 13 | 14 | public async run(message: Message): Promise { 15 | return message.replyLocale('COMMAND_INVITE'); 16 | } 17 | 18 | public async init(): Promise { 19 | if (this.client.application && !this.client.application.botPublic) this.permissionLevel = 10; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/General/Chat Bot Info/ping.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandStore } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class extends Command { 6 | 7 | constructor(store: CommandStore, directory: string, files: string[]) { 8 | super(store, directory, files, { 9 | guarded: true, 10 | description: language => language.get('COMMAND_PING_DESCRIPTION') 11 | }); 12 | } 13 | 14 | async run(message: Message): Promise { 15 | const [msg] = await message.replyLocale('COMMAND_PING'); 16 | return message.replyLocale('COMMAND_PINGPONG', [(msg.editedTimestamp || msg.createdTimestamp) - (message.editedTimestamp || message.createdTimestamp), Math.round(this.client.ws.ping)]); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/General/Chat Bot Info/stats.ts: -------------------------------------------------------------------------------- 1 | import { version as klasaVersion, Command, CommandStore } from 'klasa'; 2 | import { version as coreVersion, Message } from '@klasa/core'; 3 | import { Duration } from '@klasa/duration'; 4 | import { codeblock } from 'discord-md-tags'; 5 | 6 | export default class extends Command { 7 | 8 | public constructor(store: CommandStore, directory: string, files: readonly string[]) { 9 | super(store, directory, files, { 10 | guarded: true, 11 | description: language => language.get('COMMAND_STATS_DESCRIPTION') 12 | }); 13 | } 14 | 15 | public async run(message: Message): Promise { 16 | return message.reply(mb => mb 17 | .setContent(codeblock('asciidoc') `${message.language.get('COMMAND_STATS', 18 | (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2), 19 | Duration.toNow(Date.now() - (process.uptime() * 1000)), 20 | this.client.users.size.toLocaleString(), 21 | this.client.guilds.size.toLocaleString(), 22 | this.client.channels.size.toLocaleString(), 23 | klasaVersion, coreVersion, process.version, message 24 | )}`) 25 | ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/events/argumentError.ts: -------------------------------------------------------------------------------- 1 | import { Event, Message } from '@klasa/core'; 2 | import { codeblock } from 'discord-md-tags'; 3 | 4 | import type { Argument } from 'klasa'; 5 | 6 | export default class extends Event { 7 | 8 | public async run(message: Message, argument: Argument, _params: readonly unknown[], error: Error | string): Promise { 9 | if (error instanceof Error) this.client.emit('wtf', `[ARGUMENT] ${argument.path}\n${error.stack || error}`); 10 | if (typeof error === 'string') await message.reply(mb => mb.setContent(error)); 11 | else await message.reply(mb => mb.setContent(codeblock('JSON') `${error.message}`)); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/events/commandError.ts: -------------------------------------------------------------------------------- 1 | import { Event, Message } from '@klasa/core'; 2 | import { codeblock } from 'discord-md-tags'; 3 | 4 | import type { Command } from 'klasa'; 5 | 6 | export default class extends Event { 7 | 8 | public async run(message: Message, command: Command, _params: readonly unknown[], error: Error | string): Promise { 9 | if (error instanceof Error) this.client.emit('wtf', `[COMMAND] ${command.path}\n${error.stack || error}`); 10 | if (typeof error === 'string') await message.reply(mb => mb.setContent(error)); 11 | else await message.reply(mb => mb.setContent(codeblock('JSON') `${error.message}`)); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/events/commandInhibited.ts: -------------------------------------------------------------------------------- 1 | import { Event, Message } from '@klasa/core'; 2 | 3 | import type { Command } from 'klasa'; 4 | 5 | export default class extends Event { 6 | 7 | public async run(message: Message, _command: Command, response: string): Promise { 8 | if (response && response.length) await message.reply(mb => mb.setContent(response)); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/events/coreGuildDelete.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventStore, Guild } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public constructor(store: EventStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { event: 'guildDelete' }); 7 | } 8 | 9 | public async run(guild: Guild): Promise { 10 | if (!guild.unavailable && !this.client.options.settings.preserve) await guild.settings.destroy(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/coreMessageCreate.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventStore, Message } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public constructor(store: EventStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { event: 'messageCreate' }); 7 | } 8 | 9 | public run(message: Message): void { 10 | this.client.monitors.run(message); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/coreMessageDelete.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventStore, Message } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public constructor(store: EventStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { event: 'messageDelete' }); 7 | } 8 | 9 | public async run(message: Message): Promise { 10 | if (message.command && message.command.deletable) { 11 | for (const msg of message.responses) { 12 | if (!msg.deleted) await msg.delete(); 13 | } 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/events/coreMessageDeleteBulk.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventStore, Message } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public constructor(store: EventStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { event: 'messageDeleteBulk' }); 7 | } 8 | 9 | public async run(messages: Message[]): Promise { 10 | for (const message of messages.values()) { 11 | if (message.command && message.command.deletable) { 12 | for (const msg of message.responses) { 13 | if (!msg.deleted) await msg.delete(); 14 | } 15 | } 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/events/coreMessageUpdate.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventStore, Message } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public constructor(store: EventStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { event: 'messageUpdate' }); 7 | } 8 | 9 | public run(message: Message, previous: Message): void { 10 | if (previous.content !== message.content) this.client.monitors.run(message); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/debug.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public run(warning: Error): void { 6 | this.client.console.debug(warning); 7 | } 8 | 9 | public init(): void { 10 | if (!this.client.options.consoleEvents.debug) this.disable(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/disconnect.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public run(error: { code: number, reason: string }): void { 6 | this.client.emit('error', `Disconnected | ${error.code}: ${error.reason}`); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/events/error.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public run(error: Error): void { 6 | this.client.console.error(error); 7 | } 8 | 9 | public init(): void { 10 | if (!this.client.options.consoleEvents.error) this.disable(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/eventError.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public run(event: Event, _args: readonly unknown[], error: Error): void { 6 | this.client.emit('wtf', `[EVENT] ${event.path}\n${error ? 7 | error.stack ? error.stack : error : 'Unknown error'}`); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/events/finalizerError.ts: -------------------------------------------------------------------------------- 1 | import { Event, Message } from '@klasa/core'; 2 | 3 | import type { Command, Finalizer } from 'klasa'; 4 | import type { Stopwatch } from '@klasa/stopwatch'; 5 | 6 | export default class extends Event { 7 | 8 | public run(_message: Message, _command: Command, _response: Message[], _timer: Stopwatch, finalizer: Finalizer, error: Error): void { 9 | this.client.emit('wtf', `[FINALIZER] ${finalizer.path}\n${error ? 10 | error.stack ? error.stack : error : 'Unknown error'}`); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/log.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public run(data: string): void { 6 | this.client.console.log(data); 7 | } 8 | 9 | public init(): void { 10 | if (!this.client.options.consoleEvents.log) this.disable(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/monitorError.ts: -------------------------------------------------------------------------------- 1 | import { Event, Message } from '@klasa/core'; 2 | 3 | import type { Monitor } from 'klasa'; 4 | 5 | export default class extends Event { 6 | 7 | public run(_message: Message, monitor: Monitor, error: Error): void { 8 | this.client.emit('wtf', `[MONITOR] ${monitor.path}\n${error ? 9 | error.stack ? error.stack : error : 'Unknown error'}`); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/events/taskError.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | import type { Task, ScheduledTask } from 'klasa'; 4 | 5 | export default class extends Event { 6 | 7 | public run(_scheduledTask: ScheduledTask, task: Task, error: Error): void { 8 | this.client.emit('wtf', `[TASK] ${task.path}\n${error ? 9 | error.stack ? error.stack : error : 'Unknown error'}`); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/events/unhandledRejection.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventStore } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public constructor(store: EventStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { emitter: process }); 7 | if (this.client.options.production) this.unload(); 8 | } 9 | 10 | public run(error: Error): void { 11 | if (!error) return; 12 | this.client.emit('error', `Uncaught Promise Error: \n${error.stack || error}`); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/events/verbose.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public run(log: string): void { 6 | this.client.console.verbose(log); 7 | } 8 | 9 | public init(): void { 10 | if (!this.client.options.consoleEvents.verbose) this.disable(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/warn.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public run(warning: string | Error): void { 6 | this.client.console.warn(warning); 7 | } 8 | 9 | public init(): void { 10 | if (!this.client.options.consoleEvents.warn) this.disable(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/events/wtf.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@klasa/core'; 2 | 3 | export default class extends Event { 4 | 5 | public run(failure: string | Error): void { 6 | this.client.console.wtf(failure); 7 | } 8 | 9 | public init(): void { 10 | if (!this.client.options.consoleEvents.wtf) this.disable(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/finalizers/commandLogging.ts: -------------------------------------------------------------------------------- 1 | import { Finalizer, Command } from 'klasa'; 2 | import { ChannelType } from '@klasa/dapi-types'; 3 | import { Colors } from '@klasa/console'; 4 | 5 | import type { Message } from '@klasa/core'; 6 | import type { Stopwatch } from '@klasa/stopwatch'; 7 | 8 | export default class extends Finalizer { 9 | 10 | private reprompted = [new Colors({ background: 'blue' }), new Colors({ background: 'red' })]; 11 | private user = new Colors({ background: 'yellow', text: 'black' }); 12 | private shard = new Colors({ background: 'cyan', text: 'black' }); 13 | private dm = new Colors({ background: 'magenta' }); 14 | private text = new Colors({ background: 'green', text: 'black' }) 15 | 16 | public run(message: Message, command: Command, _response: Message[], timer: Stopwatch): void { 17 | const shard = message.guild ? message.guild.shard.id : 0; 18 | this.client.emit('log', [ 19 | this.shard.format(`[${shard}]`), 20 | `${command.name}(${message.args.join(', ')})`, 21 | this.reprompted[Number(message.reprompted)].format(`[${timer.stop()}]`), 22 | this.user.format(`${message.author.username}[${message.author.id}]`), 23 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 24 | message.channel.type === ChannelType.DM ? this.dm.format('Direct Messages') : this.text.format(`${message.guild!.name}[${message.guild!.id}]`) 25 | ].join(' ')); 26 | } 27 | 28 | public init(): void { 29 | this.enabled = this.client.options.commands.logging; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // KlasaClient 2 | export * from './lib/Client'; 3 | 4 | // lib/extensions 5 | export * from './lib/extensions/KlasaGuild'; 6 | export * from './lib/extensions/KlasaMessage'; 7 | export * from './lib/extensions/KlasaUser'; 8 | 9 | // lib/permissions 10 | export * from './lib/permissions/PermissionLevels'; 11 | 12 | // lib/schedule 13 | export * from './lib/schedule/Schedule'; 14 | export * from './lib/schedule/ScheduledTask'; 15 | 16 | // lib/settings 17 | export * from './lib/settings/gateway/Gateway'; 18 | export * from './lib/settings/gateway/GatewayStore'; 19 | export * from './lib/settings/schema/Schema'; 20 | export * from './lib/settings/schema/SchemaEntry'; 21 | export * from './lib/settings/Settings'; 22 | 23 | // lib/structures 24 | export * from './lib/structures/Argument'; 25 | export * from './lib/structures/ArgumentStore'; 26 | export * from './lib/structures/Command'; 27 | export * from './lib/structures/CommandStore'; 28 | export * from './lib/structures/Extendable'; 29 | export * from './lib/structures/ExtendableStore'; 30 | export * from './lib/structures/Finalizer'; 31 | export * from './lib/structures/FinalizerStore'; 32 | export * from './lib/structures/Inhibitor'; 33 | export * from './lib/structures/InhibitorStore'; 34 | export * from './lib/structures/Language'; 35 | export * from './lib/structures/LanguageStore'; 36 | export * from './lib/structures/Monitor'; 37 | export * from './lib/structures/MonitorStore'; 38 | export * from './lib/structures/MultiArgument'; 39 | export * from './lib/structures/Provider'; 40 | export * from './lib/structures/ProviderStore'; 41 | export * from './lib/structures/Serializer'; 42 | export * from './lib/structures/SerializerStore'; 43 | export * from './lib/structures/SQLProvider'; 44 | export * from './lib/structures/Task'; 45 | export * from './lib/structures/TaskStore'; 46 | 47 | // lib/usage 48 | export * from './lib/usage/CommandPrompt'; 49 | export * from './lib/usage/CommandUsage'; 50 | export * from './lib/usage/Usage'; 51 | export * from './lib/usage/Possible'; 52 | export * from './lib/usage/Tag'; 53 | export * from './lib/usage/TextPrompt'; 54 | 55 | // lib/util 56 | export * from './lib/util/constants'; 57 | export * from './lib/util/QueryBuilder'; 58 | export * from './lib/util/ReactionHandler'; 59 | export * from './lib/util/RichDisplay'; 60 | export * from './lib/util/RichMenu'; 61 | -------------------------------------------------------------------------------- /src/inhibitors/disabled.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor, Command } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class extends Inhibitor { 6 | 7 | public run(message: Message, command: Command): void { 8 | if (!command.enabled) throw message.language.get('INHIBITOR_DISABLED_GLOBAL'); 9 | if ((message.guildSettings.get('disabledCommands') as string[]).includes(command.name)) throw message.language.get('INHIBITOR_DISABLED_GUILD'); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/inhibitors/hidden.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor, Command } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class extends Inhibitor { 6 | 7 | public run(message: Message, command: Command): boolean { 8 | return command.hidden && message.command !== command && !this.client.owners.has(message.author); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/inhibitors/missingBotPermissions.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor, Command } from 'klasa'; 2 | import { Permissions, PermissionsFlags, Message, isGuildTextBasedChannel } from '@klasa/core'; 3 | import { toTitleCase } from '@klasa/utils'; 4 | 5 | export default class extends Inhibitor { 6 | 7 | private readonly impliedPermissions = new Permissions(515136).freeze(); 8 | // VIEW_CHANNEL, SEND_MESSAGES, SEND_TTS_MESSAGES, EMBED_LINKS, ATTACH_FILES, 9 | // READ_MESSAGE_HISTORY, MENTION_EVERYONE, USE_EXTERNAL_EMOJIS, ADD_REACTIONS 10 | 11 | private readonly friendlyPerms = Object.keys(Permissions.FLAGS).reduce((obj, key) => { 12 | Reflect.set(obj, key, toTitleCase(key.split('_').join(' '))); 13 | return obj; 14 | }, {}) as Record; 15 | 16 | public run(message: Message, command: Command): void { 17 | const missing: PermissionsFlags[] = isGuildTextBasedChannel(message.channel) ? 18 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 19 | (message.guild!.me!.permissionsIn(message.channel).missing(command.requiredPermissions) ?? []) as PermissionsFlags[] : 20 | this.impliedPermissions.missing(command.requiredPermissions) as PermissionsFlags[]; 21 | 22 | if (missing.length) throw message.language.get('INHIBITOR_MISSING_BOT_PERMS', missing.map(key => this.friendlyPerms[key]).join(', ')); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/inhibitors/nsfw.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor, Command } from 'klasa'; 2 | import { ChannelType } from '@klasa/dapi-types'; 3 | 4 | import type { Message } from '@klasa/core'; 5 | 6 | export default class extends Inhibitor { 7 | 8 | public run(message: Message, command: Command): void { 9 | if (command.nsfw && message.channel.type !== ChannelType.DM && !message.channel.nsfw) throw message.language.get('INHIBITOR_NSFW'); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/inhibitors/permissions.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor, Command } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class extends Inhibitor { 6 | 7 | public async run(message: Message, command: Command): Promise { 8 | const { broke, permission } = await this.client.permissionLevels.run(message, command.permissionLevel); 9 | if (!permission) throw broke ? message.language.get('INHIBITOR_PERMISSIONS') : true; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/inhibitors/requiredSettings.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor, Command } from 'klasa'; 2 | import { ChannelType } from '@klasa/dapi-types'; 3 | 4 | import type { Message } from '@klasa/core'; 5 | 6 | const GuildTextBasedChannels = [ChannelType.GuildNews, ChannelType.GuildText]; 7 | 8 | export default class extends Inhibitor { 9 | 10 | public run(message: Message, command: Command): void { 11 | if (!command.requiredSettings.length || !GuildTextBasedChannels.includes(message.channel.type)) return; 12 | // eslint-disable-next-line eqeqeq, @typescript-eslint/no-non-null-assertion 13 | const requiredSettings = command.requiredSettings.filter(setting => message.guild!.settings.get(setting) == null); 14 | if (requiredSettings.length) throw message.language.get('INHIBITOR_REQUIRED_SETTINGS', requiredSettings); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/inhibitors/runIn.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor, Command } from 'klasa'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | 5 | export default class extends Inhibitor { 6 | 7 | public run(message: Message, command: Command): void { 8 | if (!command.runIn.length) throw message.language.get('INHIBITOR_RUNIN_NONE', command.name); 9 | if (!command.runIn.includes(message.channel.type)) throw message.language.get('INHIBITOR_RUNIN', command.runIn.join(', ')); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/inhibitors/slowmode.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor, InhibitorStore } from 'klasa'; 2 | import { RateLimitManager } from '@klasa/ratelimits'; 3 | 4 | import type { Message } from '@klasa/core'; 5 | 6 | export default class extends Inhibitor { 7 | 8 | private readonly slowmode: RateLimitManager; 9 | private readonly aggressive: boolean; 10 | 11 | constructor(store: InhibitorStore, directory: string, files: readonly string[]) { 12 | super(store, directory, files, { spamProtection: true }); 13 | this.slowmode = new RateLimitManager(this.client.options.commands.slowmode); 14 | this.aggressive = this.client.options.commands.slowmodeAggressive; 15 | 16 | if (!this.client.options.commands.slowmode) this.disable(); 17 | } 18 | 19 | public run(message: Message): void { 20 | if (this.client.owners.has(message.author)) return; 21 | 22 | const rateLimit = this.slowmode.acquire(message.author.id); 23 | 24 | try { 25 | rateLimit.consume(); 26 | } catch (err) { 27 | if (this.aggressive) rateLimit.resetTime(); 28 | throw true; 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/extensions/KlasaGuild.ts: -------------------------------------------------------------------------------- 1 | import { extender } from '@klasa/core'; 2 | 3 | import type { Settings } from '../settings/Settings'; 4 | import type { Language } from '../structures/Language'; 5 | import type { Gateway } from '../settings/gateway/Gateway'; 6 | 7 | /** 8 | * Klasa's Extended Guild 9 | * @extends external:Guild 10 | */ 11 | export class KlasaGuild extends extender.get('Guild') { 12 | 13 | /** 14 | * The guild level settings for this context (guild || default) 15 | * @since 0.5.0 16 | */ 17 | public settings: Settings; 18 | 19 | public constructor(...args: any[]) { 20 | super(...args); 21 | 22 | this.settings = (this.client.gateways.get('guilds') as Gateway).acquire(this); 23 | } 24 | 25 | /** 26 | * The language configured for this guild 27 | */ 28 | public get language(): Language { 29 | return this.client.languages.get(this.settings.get('language') as string) as Language; 30 | } 31 | 32 | /** 33 | * Returns the JSON-compatible object of this instance. 34 | * @since 0.5.0 35 | */ 36 | public toJSON(): Record { 37 | return { ...super.toJSON(), settings: this.settings.toJSON() }; 38 | } 39 | 40 | } 41 | 42 | extender.extend('Guild', () => KlasaGuild); 43 | 44 | declare module '@klasa/core/dist/src/lib/caching/structures/guilds/Guild' { 45 | 46 | export interface Guild { 47 | settings: Settings; 48 | language: Language; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/extensions/KlasaUser.ts: -------------------------------------------------------------------------------- 1 | import { extender } from '@klasa/core'; 2 | 3 | import type { Settings } from '../settings/Settings'; 4 | import type { Gateway } from '../settings/gateway/Gateway'; 5 | 6 | /** 7 | * Klasa's Extended User 8 | * @extends external:User 9 | */ 10 | export class KlasaUser extends extender.get('User') { 11 | 12 | /** 13 | * The user level settings for this context (user || default) 14 | * @since 0.5.0 15 | */ 16 | public settings: Settings; 17 | 18 | public constructor(...args: readonly unknown[]) { 19 | super(...args); 20 | 21 | this.settings = (this.client.gateways.get('users') as Gateway).acquire(this); 22 | } 23 | 24 | /** 25 | * Returns the JSON-compatible object of this instance. 26 | * @since 0.5.0 27 | */ 28 | public toJSON(): Record { 29 | return { ...super.toJSON(), settings: this.settings }; 30 | } 31 | 32 | } 33 | 34 | extender.extend('User', () => KlasaUser); 35 | 36 | declare module '@klasa/core/dist/src/lib/caching/structures/User' { 37 | 38 | export interface User { 39 | settings: Settings; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/settings/gateway/GatewayStore.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@klasa/cache'; 2 | 3 | import type { Client } from '@klasa/core'; 4 | import type { Gateway, GatewayJson } from './Gateway'; 5 | 6 | export class GatewayStore extends Cache { 7 | 8 | /** 9 | * The client this GatewayDriver was created with. 10 | * @since 0.6.0 11 | */ 12 | public readonly client: Client; 13 | 14 | /** 15 | * Constructs a new instance of GatewayDriver. 16 | * @param client The client that manages this instance 17 | */ 18 | public constructor(client: Client) { 19 | super(); 20 | this.client = client; 21 | } 22 | 23 | /** 24 | * Registers a new gateway. 25 | * @param gateway The gateway to register 26 | * @example 27 | * // Import Client and Gateway from klasa 28 | * const { Client, Gateway } = require('klasa'); 29 | * 30 | * // Construct the client and register a gateway named channels 31 | * const client = new Client(); 32 | * client.register(new Gateway(client, 'channels')); 33 | * 34 | * @example 35 | * // Import Client and Gateway from klasa 36 | * const { Client, Gateway } = require('klasa'); 37 | * const client = new Client(); 38 | * 39 | * // register calls can be chained 40 | * client 41 | * .register(new Gateway(client, 'channels')) 42 | * .register(new Gateway(client, 'moderations', { provider: 'postgres' })); 43 | */ 44 | public register(gateway: Gateway): this { 45 | this.set(gateway.name, gateway); 46 | return this; 47 | } 48 | 49 | /** 50 | * Initializes all gateways. 51 | */ 52 | public async init(): Promise { 53 | await Promise.all(this.map(gateway => gateway.init())); 54 | } 55 | 56 | /** 57 | * The gateway driver with all serialized gateways. 58 | */ 59 | public toJSON(): GatewayDriverJson { 60 | return Object.fromEntries(this.map((value, key) => [key, value.toJSON()])); 61 | } 62 | 63 | } 64 | 65 | export type GatewayDriverJson = Record; 66 | -------------------------------------------------------------------------------- /src/lib/settings/schema/Schema.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@klasa/cache'; 2 | import { SchemaEntry, SchemaEntryOptions, SchemaEntryJson } from './SchemaEntry'; 3 | import { Settings, SettingsExistenceStatus } from '../Settings'; 4 | 5 | /* eslint-disable no-dupe-class-members */ 6 | 7 | export class Schema extends Cache { 8 | 9 | /** 10 | * The defaults for this schema. 11 | */ 12 | public readonly defaults: Settings; 13 | 14 | /** 15 | * Whether or not this instance is ready. 16 | */ 17 | public ready: boolean; 18 | 19 | /** 20 | * Constructs the schema 21 | */ 22 | public constructor() { 23 | super(); 24 | 25 | this.ready = false; 26 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 27 | // @ts-expect-error 28 | this.defaults = new Settings({ schema: this }, null, ''); 29 | this.defaults.existenceStatus = SettingsExistenceStatus.Defaults; 30 | } 31 | 32 | /** 33 | * Adds or replaces an entry to this instance. 34 | * @param key The key of the entry to add 35 | * @param value The entry to add 36 | */ 37 | public set(key: string, value: SchemaEntry): this { 38 | if (this.ready) throw new Error('Cannot modify the schema after being initialized.'); 39 | this.defaults.set(key, value.default); 40 | return super.set(key, value); 41 | } 42 | 43 | /** 44 | * Removes an entry from this instance. 45 | * @param key The key of the element to remove 46 | */ 47 | public delete(key: string): boolean { 48 | if (this.ready) throw new Error('Cannot modify the schema after being initialized.'); 49 | this.defaults.delete(key); 50 | return super.delete(key); 51 | } 52 | 53 | /** 54 | * Add a new entry to the schema. 55 | * @param key The name for the key to add 56 | * @param type The datatype, will be lowercased in the instance 57 | * @param options The options for the entry 58 | * @example 59 | * // Create a schema with a key of experience: 60 | * new Schema() 61 | * .add('experience', 'integer', { minimum: 0 }); 62 | * 63 | * @example 64 | * // Modify the built-in user schema to add experience and level: 65 | * KlasaClient.defaultUserSchema 66 | * .add('experience', 'integer', { minimum: 0 }) 67 | * .add('level', 'integer', { minimum: 0 }); 68 | */ 69 | public add(key: string, type: string, options?: SchemaEntryOptions): this { 70 | const previous = super.get(key); 71 | if (typeof previous !== 'undefined') { 72 | // Edit the previous key 73 | const schemaEntry = previous; 74 | schemaEntry.edit({ type, ...options }); 75 | this.defaults.set(key, schemaEntry.default); 76 | return this; 77 | } 78 | 79 | this.set(key, new SchemaEntry(this, key, type, options)); 80 | return this; 81 | } 82 | 83 | /** 84 | * Returns an object literal composed of all children serialized recursively. 85 | */ 86 | public toJSON(): Record { 87 | return Object.fromEntries(this.map((value, key) => [key, value.toJSON()])); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/lib/structures/Argument.ts: -------------------------------------------------------------------------------- 1 | import { AliasPiece, Client, Message } from '@klasa/core'; 2 | import { MENTION_REGEX } from '../util/constants'; 3 | 4 | import type { Possible } from '../usage/Possible'; 5 | import type { Language } from './Language'; 6 | import type { CustomUsageArgument } from '../usage/Usage'; 7 | 8 | /** 9 | * Base class for all Klasa Arguments. See {@tutorial CreatingArguments} for more information how to use this class 10 | * to build custom arguments. 11 | * @tutorial CreatingArguments 12 | */ 13 | export abstract class Argument extends AliasPiece { 14 | 15 | /** 16 | * The run method to be overwritten in actual Arguments 17 | * @since 0.5.0 18 | * @param argument The string argument string to resolve 19 | * @param possible This current usage possible 20 | * @param message The message that triggered the command 21 | */ 22 | public abstract run(argument: string, possible: Possible, message: Message, custom?: CustomUsageArgument): unknown | Promise; 23 | 24 | /** 25 | * Checks min and max values 26 | * @since 0.5.0 27 | * @param client The client of this bot 28 | * @param value The value to check against 29 | * @param min The minimum value 30 | * @param max The maximum value 31 | * @param possible The id of the current possible usage 32 | * @param message The message that triggered the command 33 | * @param suffix An error suffix 34 | */ 35 | protected static minOrMax(client: Client, value: number, min: number | null = null, max: number | null = null, possible: Possible, message: Message, suffix?: string): boolean { 36 | const language = (message ? message.language : client.languages.default) as Language; 37 | suffix = suffix ? language.get(suffix) as string : ''; 38 | if (min !== null && max !== null) { 39 | if (value >= min && value <= max) return true; 40 | if (min === max) throw language.get('RESOLVER_MINMAX_EXACTLY', possible.name, min, suffix); 41 | throw language.get('RESOLVER_MINMAX_BOTH', possible.name, min, max, suffix); 42 | } else if (min !== null) { 43 | if (value >= min) return true; 44 | throw language.get('RESOLVER_MINMAX_MIN', possible.name, min, suffix); 45 | } else if (max !== null) { 46 | if (value <= max) return true; 47 | throw language.get('RESOLVER_MINMAX_MAX', possible.name, max, suffix); 48 | } 49 | return true; 50 | } 51 | 52 | /** 53 | * Standard regular expressions for matching mentions and snowflake ids 54 | * @since 0.5.0 55 | */ 56 | public static regex = MENTION_REGEX; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/structures/ArgumentStore.ts: -------------------------------------------------------------------------------- 1 | import { AliasStore, PieceConstructor, Client } from '@klasa/core'; 2 | import { Argument } from './Argument'; 3 | 4 | /** 5 | * Stores all {@link Argument} pieces for use in Klasa. 6 | * @since 0.0.1 7 | */ 8 | export class ArgumentStore extends AliasStore { 9 | 10 | /** 11 | * Constructs our ArgumentStore for use in Klasa. 12 | * @since 0.0.1 13 | * @param client The Klasa client 14 | */ 15 | public constructor(client: Client) { 16 | super(client, 'arguments', Argument as unknown as PieceConstructor); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/structures/CommandStore.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './Command'; 2 | import { AliasStore, PieceConstructor, Client } from '@klasa/core'; 3 | 4 | /** 5 | * Stores all {@link Command} pieces for use in Klasa. 6 | * @since 0.0.1 7 | */ 8 | export class CommandStore extends AliasStore { 9 | 10 | /** 11 | * Constructs our CommandStore for use in Klasa. 12 | * @since 0.0.1 13 | * @param client The Klasa client 14 | */ 15 | public constructor(client: Client) { 16 | super(client, 'commands', Command as PieceConstructor); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/structures/ExtendableStore.ts: -------------------------------------------------------------------------------- 1 | import { Extendable } from './Extendable'; 2 | import { Store, PieceConstructor, Client } from '@klasa/core'; 3 | 4 | /** 5 | * Stores all {@link Extendable} pieces for use in Klasa. 6 | * @since 0.0.1 7 | */ 8 | export class ExtendableStore extends Store { 9 | 10 | /** 11 | * Constructs our ExtendableStore for use in Klasa. 12 | * @since 0.0.1 13 | * @param client The Klasa client 14 | */ 15 | public constructor(client: Client) { 16 | super(client, 'extendables', Extendable as PieceConstructor); 17 | } 18 | 19 | /** 20 | * Deletes an extendable from the store. 21 | * @since 0.0.1 22 | * @param name A extendable object or a string representing a command or alias name 23 | */ 24 | public remove(name: Extendable | string): boolean { 25 | const extendable = this.resolve(name); 26 | if (!extendable) return false; 27 | extendable.disable(); 28 | return super.remove(extendable); 29 | } 30 | 31 | /** 32 | * Clears the extendable from the store and removes the extensions. 33 | * @since 0.0.1 34 | */ 35 | public clear(): void { 36 | for (const extendable of this.values()) this.remove(extendable); 37 | } 38 | 39 | /** 40 | * Sets up an extendable in our store. 41 | * @since 0.0.1 42 | * @param piece The extendable piece we are setting up 43 | */ 44 | public add(piece: Extendable): Extendable | null { 45 | const extendable = super.add(piece); 46 | if (!extendable) return null; 47 | extendable.init(); 48 | return extendable; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/structures/Finalizer.ts: -------------------------------------------------------------------------------- 1 | import { Piece, Message } from '@klasa/core'; 2 | 3 | import type { Stopwatch } from '@klasa/stopwatch'; 4 | import type { Command } from './Command'; 5 | 6 | /** 7 | * Base class for all Klasa Finalizers. See {@tutorial CreatingFinalizers} for more information how to use this class 8 | * to build custom finalizers. 9 | * @tutorial CreatingFinalizers 10 | * @extends {Piece} 11 | */ 12 | export abstract class Finalizer extends Piece { 13 | 14 | /** 15 | * The run method to be overwritten in actual finalizers 16 | * @since 0.0.1 17 | * @param message The message used to trigger this finalizer 18 | * @param command The command this finalizer is for (may be different than message.command) 19 | * @param responses The bot's response message, if one is returned 20 | * @param runTime The time it took to generate the command 21 | */ 22 | public abstract run(message: Message, command: Command, responses: Message[] | undefined, runTime: Stopwatch): Promise | unknown; 23 | 24 | /** 25 | * Run a finalizer and catch any uncaught promises 26 | * @since 0.5.0 27 | * @param message The message that called the command 28 | * @param command The command this finalizer is for (may be different than message.command) 29 | * @param responses The bot's response message, if one is returned 30 | * @param runTime The time it took to generate the command 31 | */ 32 | protected async _run(message: Message, command: Command, responses: Message[] | undefined, runTime: Stopwatch): Promise { 33 | try { 34 | await this.run(message, command, responses, runTime); 35 | } catch (err) { 36 | this.client.emit('finalizerError', message, command, responses, runTime, this, err); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/structures/FinalizerStore.ts: -------------------------------------------------------------------------------- 1 | import { Store, PieceConstructor, Client, Message } from '@klasa/core'; 2 | import { Finalizer } from './Finalizer'; 3 | 4 | import type { Stopwatch } from '@klasa/stopwatch'; 5 | import type { Command } from './Command'; 6 | 7 | /** 8 | * Stores all {@link Finalizer} pieces for use in Klasa. 9 | * @since 0.0.1 10 | */ 11 | export class FinalizerStore extends Store { 12 | 13 | /** 14 | * Constructs our FinalizerStore for use in Klasa. 15 | * @since 0.0.1 16 | * @param client The Klasa client 17 | */ 18 | public constructor(client: Client) { 19 | super(client, 'finalizers', Finalizer as PieceConstructor); 20 | } 21 | 22 | /** 23 | * Runs all of our finalizers after a command is ran successfully. 24 | * @since 0.0.1 25 | * @param message The message that called the command 26 | * @param command The command this finalizer is for (may be different than message.command) 27 | * @param responses The responses of the command 28 | * @param timer The timer run from start to queue of the command 29 | */ 30 | public run(message: Message, command: Command, responses: Message[], timer: Stopwatch): void { 31 | // eslint-disable-next-line dot-notation 32 | for (const finalizer of this.values()) if (finalizer.enabled) finalizer['_run'](message, command, responses, timer); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/structures/Inhibitor.ts: -------------------------------------------------------------------------------- 1 | import { Piece, PieceOptions, Message } from '@klasa/core'; 2 | 3 | import type { InhibitorStore } from './InhibitorStore'; 4 | import type { Command } from './Command'; 5 | 6 | /** 7 | * Base class for all Klasa Inhibitors. See {@tutorial CreatingInhibitors} for more information how to use this class 8 | * to build custom inhibitors. 9 | * @tutorial CreatingInhibitors 10 | * @extends Piece 11 | */ 12 | export abstract class Inhibitor extends Piece { 13 | 14 | /** 15 | * If this inhibitor is meant for spamProtection (disables the inhibitor while generating help) 16 | * @since 0.0.1 17 | */ 18 | public spamProtection: boolean; 19 | 20 | /** 21 | * @since 0.0.1 22 | * @param store The Inhibitor Store 23 | * @param file The path from the pieces folder to the inhibitor file 24 | * @param directory The base directory to the pieces folder 25 | * @param options Optional Inhibitor settings 26 | */ 27 | public constructor(store: InhibitorStore, directory: string, files: readonly string[], options: InhibitorOptions = {}) { 28 | super(store, directory, files, options); 29 | this.spamProtection = options.spamProtection ?? false; 30 | } 31 | 32 | /** 33 | * The async wrapper for running inhibitors 34 | * @since 0.5.0 35 | * @param message The message that triggered this inhibitor 36 | * @param command The command to run 37 | */ 38 | protected async _run(message: Message, command: Command): Promise { 39 | try { 40 | return await this.run(message, command); 41 | } catch (err) { 42 | return err; 43 | } 44 | } 45 | 46 | /** 47 | * The run method to be overwritten in actual inhibitors 48 | * @since 0.0.1 49 | * @param message The message that triggered this inhibitor 50 | * @param command The command to run 51 | */ 52 | public abstract run(message: Message, command: Command): boolean | string | void | Promise; 53 | 54 | /** 55 | * Defines the JSON.stringify behavior of this inhibitor. 56 | */ 57 | toJSON(): Record { 58 | return { 59 | ...super.toJSON(), 60 | spamProtection: this.spamProtection 61 | }; 62 | } 63 | 64 | } 65 | 66 | export interface InhibitorOptions extends PieceOptions { 67 | spamProtection?: boolean; 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/structures/InhibitorStore.ts: -------------------------------------------------------------------------------- 1 | import { Inhibitor } from './Inhibitor'; 2 | import { Store, PieceConstructor, Client, Message } from '@klasa/core'; 3 | 4 | import type { Command } from './Command'; 5 | 6 | /** 7 | * Stores all {@link Inhibitor} pieces for use in Klasa 8 | */ 9 | export class InhibitorStore extends Store { 10 | 11 | /** 12 | * Constructs our InhibitorStore for use in Klasa. 13 | * @since 0.0.1 14 | * @param client The Klasa client 15 | */ 16 | public constructor(client: Client) { 17 | super(client, 'inhibitors', Inhibitor as PieceConstructor); 18 | } 19 | 20 | /** 21 | * Runs our inhibitors on the command. 22 | * @since 0.0.1 23 | * @param message The message object from @klasa/core 24 | * @param command The command being ran. 25 | * @param selective Whether or not we should ignore certain inhibitors to prevent spam. 26 | */ 27 | public async run(message: Message, command: Command, selective = false): Promise { 28 | const mps = []; 29 | // eslint-disable-next-line dot-notation 30 | for (const inhibitor of this.values()) if (inhibitor.enabled && (!selective || !inhibitor.spamProtection)) mps.push(inhibitor['_run'](message, command)); 31 | const results = (await Promise.all(mps)).filter(res => res); 32 | if (results.includes(true)) throw undefined; 33 | if (results.length) throw results; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/structures/Language.ts: -------------------------------------------------------------------------------- 1 | import { pathExists } from 'fs-nextra'; 2 | import { join } from 'path'; 3 | import { Piece, PieceConstructor } from '@klasa/core'; 4 | import { mergeDefault, isClass } from '@klasa/utils'; 5 | 6 | import type { LanguageStore } from './LanguageStore'; 7 | 8 | export type LanguageValue = string | ((...args: any[]) => string); 9 | 10 | /** 11 | * Base class for all Klasa Languages. See {@tutorial CreatingLanguages} for more information how to use this class 12 | * to build custom languages. 13 | * @tutorial CreatingLanguages 14 | */ 15 | export abstract class Language extends Piece { 16 | 17 | public abstract language: Record & { DEFAULT: (term: string) => string }; 18 | 19 | /** 20 | * The method to get language strings 21 | * @since 0.2.1 22 | * @param term The string or function to look up 23 | * @param args Any arguments to pass to the lookup 24 | */ 25 | public get(term: string, ...args: readonly unknown[]): string { 26 | if (!this.enabled && this !== this.store.default) return this.store.default.get(term, ...args); 27 | const value = this.language[term]; 28 | /* eslint-disable new-cap */ 29 | switch (typeof value) { 30 | case 'function': return value(...args); 31 | case 'undefined': 32 | if (this === this.store.default) return this.language.DEFAULT(term); 33 | return `${this.language.DEFAULT(term)}\n\n**${this.language.DEFAULT_LANGUAGE}:**\n${this.store.default.get(term, ...args)}`; 34 | default: return Array.isArray(value) ? value.join('\n') : value; 35 | } 36 | /* eslint-enable new-cap */ 37 | } 38 | 39 | /** 40 | * The init method to be optionally overwritten in actual languages 41 | * @since 0.2.1 42 | * @abstract 43 | */ 44 | async init(): Promise { 45 | // eslint-disable-next-line dot-notation 46 | for (const core of this.store['coreDirectories']) { 47 | const loc = join(core, ...this.file); 48 | if (this.directory !== core && await pathExists(loc)) { 49 | try { 50 | const loaded = await import(loc) as { default: PieceConstructor } | PieceConstructor; 51 | const LoadedPiece = 'default' in loaded ? loaded.default : loaded; 52 | if (!isClass(LoadedPiece)) return; 53 | const coreLang = new LoadedPiece(this.store, this.directory, this.file); 54 | this.language = mergeDefault(coreLang.language, this.language); 55 | } catch (error) { 56 | return; 57 | } 58 | } 59 | } 60 | return; 61 | } 62 | 63 | } 64 | 65 | export interface Language { 66 | store: LanguageStore; 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/structures/LanguageStore.ts: -------------------------------------------------------------------------------- 1 | import { Language } from './Language'; 2 | import { Store, PieceConstructor, Client } from '@klasa/core'; 3 | 4 | /** 5 | * Stores all {@link Language} pieces for use in Klasa. 6 | * @since 0.0.1 7 | */ 8 | export class LanguageStore extends Store { 9 | 10 | /** 11 | * Constructs our LanguageStore for use in Klasa. 12 | * @since 0.0.1 13 | * @param client The Klasa client 14 | */ 15 | public constructor(client: Client) { 16 | super(client, 'languages', Language as PieceConstructor); 17 | } 18 | 19 | /** 20 | * The default language set in {@link KlasaClientOptions.language} 21 | * @since 0.2.1 22 | */ 23 | public get default(): Language { 24 | return this.get(this.client.options.language) as Language; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/structures/Monitor.ts: -------------------------------------------------------------------------------- 1 | import { Piece, PieceOptions, Message } from '@klasa/core'; 2 | 3 | import type { MonitorStore } from './MonitorStore'; 4 | import type { MessageType } from '@klasa/dapi-types'; 5 | 6 | /** 7 | * Base class for all Klasa Monitors. See {@tutorial CreatingMonitors} for more information how to use this class 8 | * to build custom monitors. 9 | * @tutorial CreatingMonitors 10 | */ 11 | export abstract class Monitor extends Piece { 12 | 13 | /** 14 | * The types of messages allowed for this monitor 15 | * @since 0.5.0 16 | */ 17 | public allowedTypes: MessageType[]; 18 | 19 | /** 20 | * Whether the monitor ignores bots or not 21 | * @since 0.0.1 22 | */ 23 | public ignoreBots: boolean; 24 | 25 | /** 26 | * Whether the monitor ignores itself or not 27 | * @since 0.0.1 28 | */ 29 | public ignoreSelf: boolean; 30 | 31 | /** 32 | * Whether the monitor ignores others or not 33 | * @since 0.4.0 34 | */ 35 | public ignoreOthers: boolean; 36 | 37 | /** 38 | * Whether the monitor ignores webhooks or not 39 | * @since 0.5.0 40 | */ 41 | public ignoreWebhooks: boolean; 42 | 43 | /** 44 | * Whether the monitor ignores edits or not 45 | * @since 0.5.0 46 | */ 47 | public ignoreEdits: boolean; 48 | 49 | /** 50 | * @since 0.0.1 51 | * @param store The Monitor Store 52 | * @param directory The base directory to the pieces folder 53 | * @param files The path from the pieces folder to the monitor file 54 | * @param options Optional Monitor settings 55 | */ 56 | public constructor(store: MonitorStore, directory: string, files: readonly string[], options: MonitorOptions = {}) { 57 | super(store, directory, files, options); 58 | this.allowedTypes = options.allowedTypes as MessageType[]; 59 | this.ignoreBots = options.ignoreBots as boolean; 60 | this.ignoreSelf = options.ignoreSelf as boolean; 61 | this.ignoreOthers = options.ignoreOthers as boolean; 62 | this.ignoreWebhooks = options.ignoreWebhooks as boolean; 63 | this.ignoreEdits = options.ignoreEdits as boolean; 64 | } 65 | 66 | /** 67 | * The run method to be overwritten in actual monitor pieces 68 | * @since 0.0.1 69 | * @param message The discord message 70 | */ 71 | public abstract async run(message: Message): Promise; 72 | 73 | /** 74 | * If the monitor should run based on the filter options 75 | * @since 0.5.0 76 | * @param message The message to check 77 | */ 78 | public shouldRun(message: Message): boolean { 79 | return this.enabled && 80 | this.allowedTypes.includes(message.type) && 81 | !(this.ignoreBots && message.author.bot) && 82 | !(this.ignoreSelf && this.client.user === message.author) && 83 | !(this.ignoreOthers && this.client.user !== message.author) && 84 | !(this.ignoreWebhooks && message.webhookID) && 85 | !(this.ignoreEdits && message.editedTimestamp); 86 | } 87 | 88 | /** 89 | * Defines the JSON.stringify behavior of this monitor. 90 | * @returns {Object} 91 | */ 92 | public toJSON(): Record { 93 | return { 94 | ...super.toJSON(), 95 | ignoreBots: this.ignoreBots, 96 | ignoreSelf: this.ignoreSelf, 97 | ignoreOthers: this.ignoreOthers, 98 | ignoreWebhooks: this.ignoreWebhooks, 99 | ignoreEdits: this.ignoreEdits 100 | }; 101 | } 102 | 103 | /** 104 | * Run a monitor and catch any uncaught promises 105 | * @since 0.5.0 106 | * @param message The message object from @klasa/core 107 | */ 108 | protected async _run(message: Message): Promise { 109 | try { 110 | await this.run(message); 111 | } catch (err) { 112 | this.client.emit('monitorError', message, this, err); 113 | } 114 | } 115 | 116 | } 117 | 118 | export interface MonitorOptions extends PieceOptions { 119 | allowedTypes?: MessageType[]; 120 | ignoreBots?: boolean; 121 | ignoreSelf?: boolean; 122 | ignoreOthers?: boolean; 123 | ignoreWebhooks?: boolean; 124 | ignoreEdits?: boolean; 125 | } 126 | -------------------------------------------------------------------------------- /src/lib/structures/MonitorStore.ts: -------------------------------------------------------------------------------- 1 | import { Store, PieceConstructor, Client, Message } from '@klasa/core'; 2 | import { Monitor } from './Monitor'; 3 | 4 | /** 5 | * Stores all {@link Monitor} pieces for use in Klasa. 6 | * @since 0.0.1 7 | */ 8 | export class MonitorStore extends Store { 9 | 10 | /** 11 | * Constructs our MonitorStore for use in Klasa. 12 | * @since 0.0.1 13 | * @param client The Klasa client 14 | */ 15 | public constructor(client: Client) { 16 | super(client, 'monitors', Monitor as PieceConstructor); 17 | } 18 | 19 | /** 20 | * Runs our monitors on the message. 21 | * @since 0.0.1 22 | * @param message The message to be used in the {@link Monitor monitors}. 23 | */ 24 | public run(message: Message): void { 25 | // eslint-disable-next-line dot-notation 26 | for (const monitor of this.values()) if (monitor.shouldRun(message)) monitor['_run'](message); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/structures/MultiArgument.ts: -------------------------------------------------------------------------------- 1 | import { Argument } from './Argument'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | import type { Possible } from '../usage/Possible'; 5 | 6 | /** 7 | * Base abstracted class for multi-resolving values. 8 | */ 9 | export class MultiArgument extends Argument { 10 | 11 | /** 12 | * A getter for the base argument 13 | * @since 0.5.0 14 | */ 15 | public get base(): Argument { 16 | throw new Error('A "base" getter must be implemented in extended classes.'); 17 | } 18 | 19 | /** 20 | * The run method for handling MultiArguments (not to be implemented in extended classes) 21 | * @since 0.5.0 22 | * @param {string} argument The string argument string to resolve 23 | * @param {Possible} possible This current usage possible 24 | * @param {Message} message The message that triggered the command 25 | */ 26 | public async run(argument: string, possible: Possible, message: Message): Promise { 27 | const structures = []; 28 | const { min, max } = possible; 29 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 30 | const { args, usage: { usageDelim } } = message.prompter!; 31 | const index = args.indexOf(argument); 32 | const rest = args.splice(index, args.length - index); 33 | const { base } = this; 34 | let i = 0; 35 | 36 | for (const arg of rest) { 37 | if (max && i >= max) break; 38 | try { 39 | const structure = await base.run(arg as string, possible, message); 40 | structures.push(structure); 41 | i++; 42 | } catch (err) { 43 | break; 44 | } 45 | } 46 | 47 | args.push(rest.splice(0, structures.length).join(usageDelim), ...rest); 48 | if ((min && structures.length < min) || !structures.length) throw message.language.get(`RESOLVER_MULTI_TOO_FEW`, base.name, min); 49 | return structures; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/structures/ProviderStore.ts: -------------------------------------------------------------------------------- 1 | import { PieceConstructor, Store, Client } from '@klasa/core'; 2 | import { Provider } from './Provider'; 3 | 4 | /** 5 | * Stores all {@link Provider} pieces for use in Klasa. 6 | * @since 0.1.0 7 | */ 8 | export class ProviderStore extends Store { 9 | 10 | /** 11 | * Constructs our ProviderStore for use in Klasa. 12 | * @since 0.1.0 13 | * @param client The Klasa client 14 | */ 15 | public constructor(client: Client) { 16 | super(client, 'providers', Provider as PieceConstructor); 17 | } 18 | 19 | /** 20 | * The default provider set in ClientOptions.providers. 21 | * @since 0.1.0 22 | */ 23 | public get default(): Provider | null { 24 | return this.get(this.client.options.providers.default as string) || null; 25 | } 26 | 27 | /** 28 | * Clears the providers from the store and waits for them to shutdown. 29 | * @since 0.1.0 30 | */ 31 | public clear(): void { 32 | for (const provider of this.values()) this.remove(provider); 33 | } 34 | 35 | /** 36 | * Deletes a provider from the store. 37 | * @since 0.6.0 38 | * @param name The Provider instance or its name 39 | */ 40 | public remove(name: string | Provider): boolean { 41 | const provider = this.resolve(name); 42 | if (!provider) return false; 43 | 44 | /* istanbul ignore next: Hard to coverage test the catch */ 45 | Promise.resolve(provider.shutdown()).catch((error) => this.client.emit('wtf', error)); 46 | return super.remove(provider); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/structures/Serializer.ts: -------------------------------------------------------------------------------- 1 | import { AliasPiece, Guild } from '@klasa/core'; 2 | import { MENTION_REGEX } from '../util/constants'; 3 | 4 | import type { Language } from './Language'; 5 | import type { SchemaEntry } from '../settings/schema/SchemaEntry'; 6 | 7 | export abstract class Serializer extends AliasPiece { 8 | 9 | /** 10 | * Resolve a value given directly from the {@link Settings#update} call. 11 | * @param data The data to resolve 12 | * @param context The context in which this serializer is called 13 | */ 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | public validate(data: unknown, _context: SerializerUpdateContext): unknown { 16 | return data; 17 | } 18 | 19 | /** 20 | * Resolve a value given directly from the {@link Settings#resolve} call. 21 | * @param data The data to resolve 22 | * @param context The context in which this serializer is called 23 | */ 24 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 | public resolve(data: unknown, _context: SerializerUpdateContext): unknown { 26 | return data; 27 | } 28 | 29 | /** 30 | * The deserialize method to be overwritten in actual Serializers. 31 | * @param data The data to deserialize 32 | * @param context The context in which this serializer is called 33 | */ 34 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 35 | public deserialize(data: unknown, _context: SerializerUpdateContext): unknown { 36 | return data; 37 | } 38 | 39 | /** 40 | * The serialize method to be overwritten in actual Serializers. 41 | * @param data The data to serialize 42 | */ 43 | public serialize(data: unknown): unknown { 44 | return data; 45 | } 46 | 47 | /** 48 | * The stringify method to be overwritten in actual Serializers 49 | * @param data The data to stringify 50 | * @param guild The guild given for context in this call 51 | */ 52 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 53 | public stringify(data: unknown, _guild?: Guild | null): string { 54 | return String(data); 55 | } 56 | 57 | /** 58 | * Check the boundaries of a key's minimum or maximum. 59 | * @param value The value to check 60 | * @param entry The schema entry that manages the key 61 | * @param language The language that is used for this context 62 | */ 63 | protected static minOrMax(value: number, { minimum, maximum, inclusive, key }: SchemaEntry, language: Language): boolean { 64 | if (minimum && maximum) { 65 | if ((value >= minimum && value <= maximum && inclusive) || (value > minimum && value < maximum && !inclusive)) return true; 66 | if (minimum === maximum) throw language.get('RESOLVER_MINMAX_EXACTLY', key, minimum, inclusive); 67 | throw language.get('RESOLVER_MINMAX_BOTH', key, minimum, maximum, inclusive); 68 | } else if (minimum) { 69 | if ((value >= minimum && inclusive) || (value > minimum && !inclusive)) return true; 70 | throw language.get('RESOLVER_MINMAX_MIN', key, minimum, inclusive); 71 | } else if (maximum) { 72 | if ((value <= maximum && inclusive) || (value < maximum && !inclusive)) return true; 73 | throw language.get('RESOLVER_MINMAX_MAX', key, maximum, inclusive); 74 | } 75 | return true; 76 | } 77 | 78 | /** 79 | * Standard regular expressions for matching mentions and snowflake ids 80 | */ 81 | public static regex = MENTION_REGEX; 82 | 83 | } 84 | 85 | export interface SerializerUpdateContext { 86 | entry: SchemaEntry; 87 | language: Language; 88 | guild: Guild | null; 89 | extraContext: unknown; 90 | } 91 | -------------------------------------------------------------------------------- /src/lib/structures/SerializerStore.ts: -------------------------------------------------------------------------------- 1 | import { Serializer } from './Serializer'; 2 | import { AliasStore, PieceConstructor, Client } from '@klasa/core'; 3 | 4 | /** 5 | * Stores all {@link Serializer} pieces for use in Klasa. 6 | * @since 0.5.0 7 | */ 8 | export class SerializerStore extends AliasStore { 9 | 10 | /** 11 | * Constructs our SerializerStore for use in Klasa. 12 | * @since 0.5.0 13 | * @param client The Klasa client 14 | */ 15 | public constructor(client: Client) { 16 | super(client, 'serializers', Serializer as unknown as PieceConstructor); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/structures/Task.ts: -------------------------------------------------------------------------------- 1 | import { Piece } from '@klasa/core'; 2 | 3 | /** 4 | * Base class for all Klasa Task pieces. See {@tutorial CreatingTasks} for more information how to use this class 5 | * to build custom tasks. 6 | * @tutorial CreatingTasks 7 | * @extends {Piece} 8 | */ 9 | export abstract class Task extends Piece { 10 | 11 | /** 12 | * The run method to be overwritten in actual Task pieces 13 | * @since 0.5.0 14 | * @param data The data from the ScheduledTask instance 15 | */ 16 | public abstract async run(data: TaskData): Promise; 17 | 18 | } 19 | 20 | export interface TaskData extends Record { 21 | id: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/structures/TaskStore.ts: -------------------------------------------------------------------------------- 1 | import { Store, PieceConstructor, Client } from '@klasa/core'; 2 | import { Task } from './Task'; 3 | 4 | /** 5 | * Stores all {@link Task} pieces for use in Klasa. 6 | * @since 0.5.0 7 | */ 8 | export class TaskStore extends Store { 9 | 10 | /** 11 | * Constructs our TaskStore for use in Klasa. 12 | * @since 0.5.0 13 | * @param client The Klasa client 14 | */ 15 | public constructor(client: Client) { 16 | super(client, 'tasks', Task as PieceConstructor); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/usage/CommandPrompt.ts: -------------------------------------------------------------------------------- 1 | import { TextPrompt, TextPromptOptions } from './TextPrompt'; 2 | 3 | import type { Message } from '@klasa/core'; 4 | import type { CommandUsage } from './CommandUsage'; 5 | 6 | /** 7 | * A class to handle argument collection and parameter resolution for commands 8 | * @extends TextPrompt 9 | */ 10 | export class CommandPrompt extends TextPrompt { 11 | 12 | /** 13 | * @since 0.5.0 14 | * @param message The message for the command 15 | * @param usage The usage of the command 16 | * @param options The options for this CommandPrompt 17 | */ 18 | public constructor(message: Message, usage: CommandUsage, options: TextPromptOptions = {}) { 19 | super(message, usage, options); 20 | this.typing = this.client.options.commands.typing; 21 | // eslint-disable-next-line dot-notation 22 | this['_setup'](this.message.content.slice(this.message.prefixLength ?? undefined).trim().split(' ').slice(1).join(' ').trim()); 23 | } 24 | 25 | /** 26 | * Runs the internal validation, and re-prompts according to the settings 27 | * @since 0.5.0 28 | * @returns The parameters resolved 29 | */ 30 | public run(): Promise { 31 | return this.validateArgs(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/usage/CommandUsage.ts: -------------------------------------------------------------------------------- 1 | import { Usage } from './Usage'; 2 | import { CommandPrompt } from './CommandPrompt'; 3 | 4 | import type { Client, Message } from '@klasa/core'; 5 | import type { Command } from '../structures/Command'; 6 | import type { TextPromptOptions } from './TextPrompt'; 7 | 8 | /** 9 | * Converts usage strings into objects to compare against later 10 | */ 11 | export class CommandUsage extends Usage { 12 | 13 | /** 14 | * All names and aliases for the command 15 | * @since 0.0.1 16 | */ 17 | public names: string[]; 18 | 19 | /** 20 | * The compiled string for all names/aliases in a usage string 21 | * @since 0.0.1 22 | */ 23 | public commands: string; 24 | 25 | /** 26 | * The concatenated string of this.commands and this.deliminatedUsage 27 | * @since 0.0.1 28 | */ 29 | public nearlyFullUsage: string; 30 | 31 | /** 32 | * @since 0.0.1 33 | * @param client The klasa client 34 | * @param usageString The usage string for this command 35 | * @param usageDelim The usage deliminator for this command 36 | * @param command The command this parsed usage is for 37 | */ 38 | public constructor(client: Client, usageString: string, usageDelim: string, command: Command) { 39 | super(client, usageString, usageDelim); 40 | this.names = [command.name, ...command.aliases]; 41 | this.commands = this.names.length === 1 ? this.names[0] : `《${this.names.join('|')}》`; 42 | this.nearlyFullUsage = `${this.commands}${this.deliminatedUsage}`; 43 | } 44 | 45 | /** 46 | * Creates a CommandPrompt instance to collect and resolve arguments with 47 | * @since 0.5.0 48 | * @param message The message context from the prompt 49 | * @param options The options for the prompt 50 | */ 51 | public createPrompt(message: Message, options: TextPromptOptions = {}): CommandPrompt { 52 | return new CommandPrompt(message, this, options); 53 | } 54 | 55 | /** 56 | * Creates a full usage string including prefix and commands/aliases for documentation/help purposes 57 | * @since 0.0.1 58 | * @param message The message context for which to generate usage for 59 | */ 60 | public fullUsage(message: Message): string { 61 | let prefix = message.prefixLength ? message.content.slice(0, message.prefixLength) : message.guildSettings.get('prefix') as string; 62 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 63 | if (message.prefix === this.client.mentionPrefix) prefix = `@${this.client.user!.tag}`; 64 | else if (Array.isArray(prefix)) [prefix] = prefix; 65 | return `${prefix.length !== 1 ? `${prefix} ` : prefix}${this.nearlyFullUsage}`; 66 | } 67 | 68 | /** 69 | * Defines to string behavior of this class. 70 | * @since 0.5.0 71 | */ 72 | public toString(): string { 73 | return this.nearlyFullUsage; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/usage/Possible.ts: -------------------------------------------------------------------------------- 1 | const regexTypes = ['reg', 'regex', 'regexp']; 2 | 3 | /** 4 | * Represents a possibility in a usage Tag 5 | */ 6 | export class Possible { 7 | 8 | /** 9 | * The name of this possible 10 | * @since 0.2.1 11 | */ 12 | public name: string; 13 | 14 | /** 15 | * The type of this possible 16 | * @since 0.2.1 17 | */ 18 | public type: string; 19 | 20 | /** 21 | * The min of this possible 22 | * @since 0.2.1 23 | */ 24 | public min: number | null; 25 | 26 | /** 27 | * The max of this possible 28 | * @since 0.2.1 29 | */ 30 | public max: number | null; 31 | 32 | /** 33 | * The regex of this possible 34 | * @since 0.3.0 35 | */ 36 | public regex: RegExp | null; 37 | 38 | /** 39 | * @param regexResults The regex results from parsing the tag member 40 | * @since 0.2.1 41 | */ 42 | public constructor([, name, type = 'literal', min, max, regex, flags]: readonly string[]) { 43 | this.name = name; 44 | this.type = type; 45 | this.min = min ? (this.constructor as typeof Possible).resolveLimit(min, 'min') : null; 46 | this.max = max ? (this.constructor as typeof Possible).resolveLimit(max, 'max') : null; 47 | this.regex = regexTypes.includes(this.type) && regex ? new RegExp(regex, flags) : null; 48 | 49 | if (regexTypes.includes(this.type) && !this.regex) throw 'Regex types must include a regular expression'; 50 | } 51 | 52 | /** 53 | * Resolves a limit 54 | * @since 0.2.1 55 | * @param limit The limit to evaluate 56 | * @param limitType The type of limit 57 | */ 58 | private static resolveLimit(limit: string, limitType: string): number { 59 | const parsed = parseFloat(limit); 60 | if (Number.isNaN(parsed)) throw `${limitType} must be a number`; 61 | return parsed; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/lib/util/RichMenu.ts: -------------------------------------------------------------------------------- 1 | import { RichDisplay, RichDisplayOptions } from './RichDisplay'; 2 | import { ReactionMethods, ReactionHandler, ReactionHandlerOptions } from './ReactionHandler'; 3 | 4 | import type { Message } from '@klasa/core'; 5 | import { Cache } from '@klasa/cache'; 6 | 7 | const choiceMethods = [ 8 | ReactionMethods.One, 9 | ReactionMethods.Two, 10 | ReactionMethods.Three, 11 | ReactionMethods.Four, 12 | ReactionMethods.Five, 13 | ReactionMethods.Six, 14 | ReactionMethods.Seven, 15 | ReactionMethods.Eight, 16 | ReactionMethods.Nine, 17 | ReactionMethods.Ten 18 | ]; 19 | 20 | export interface Choice { 21 | name: string; 22 | body: string; 23 | inline: boolean; 24 | } 25 | 26 | /** 27 | * Klasa's RichMenu, for helping paginated embeds with reaction buttons 28 | */ 29 | export class RichMenu extends RichDisplay { 30 | 31 | /** 32 | * The menu choices 33 | * @since 0.6.0 34 | */ 35 | public choices: Choice[] = []; 36 | 37 | /** 38 | * If options have been paginated yet 39 | * @since 0.4.0 40 | */ 41 | private paginated = false; 42 | 43 | /** 44 | * @param options The RichDisplay options 45 | */ 46 | public constructor(options: RichDisplayOptions) { 47 | super(options); 48 | 49 | this._emojis = new Cache([ 50 | [ReactionMethods.One, '1️⃣'], 51 | [ReactionMethods.Two, '2️⃣'], 52 | [ReactionMethods.Three, '3️⃣'], 53 | [ReactionMethods.Four, '4️⃣'], 54 | [ReactionMethods.Five, '5️⃣'], 55 | [ReactionMethods.Six, '6️⃣'], 56 | [ReactionMethods.Seven, '7️⃣'], 57 | [ReactionMethods.Eight, '8️⃣'], 58 | [ReactionMethods.Nine, '9️⃣'], 59 | [ReactionMethods.Ten, '🔟'], 60 | ...this._emojis 61 | ]); 62 | } 63 | 64 | /** 65 | * You cannot directly add pages in a RichMenu 66 | * @since 0.4.0 67 | */ 68 | public addPage(): never { 69 | throw new Error('You cannot directly add pages in a RichMenu'); 70 | } 71 | 72 | /** 73 | * Adds a menu choice 74 | * @since 0.6.0 75 | * @param name The name of the choice 76 | * @param body The description of the choice 77 | * @param inline Whether the choice should be inline 78 | */ 79 | public addChoice(name: string, body: string, inline = false): this { 80 | this.choices.push({ name, body, inline }); 81 | return this; 82 | } 83 | 84 | /** 85 | * Runs this RichMenu 86 | * @since 0.4.0 87 | * @param KlasaMessage message A message to edit or use to send a new message with 88 | * @param options The options to use with this RichMenu 89 | */ 90 | public async run(message: Message, options: ReactionHandlerOptions = {}): Promise { 91 | if (this.choices.length < choiceMethods.length) { 92 | for (let i = this.choices.length; i < choiceMethods.length; i++) this._emojis.delete(choiceMethods[i]); 93 | } 94 | if (!this.paginated) this.paginate(); 95 | return super.run(message, options); 96 | } 97 | 98 | /** 99 | * Converts MenuOptions into display pages 100 | * @since 0.4.0 101 | */ 102 | private paginate(): null { 103 | const page = this.pages.length; 104 | if (this.paginated) return null; 105 | super.addPage(embed => { 106 | for (let i = 0, choice = this.choices[i + (page * 10)]; i + (page * 10) < this.choices.length && i < 10; i++, choice = this.choices[i + (page * 10)]) { 107 | embed.addField(`(${i + 1}) ${choice.name}`, choice.body, choice.inline); 108 | } 109 | return embed; 110 | }); 111 | if (this.choices.length > (page + 1) * 10) return this.paginate(); 112 | this.paginated = true; 113 | return null; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/monitors/commandHandler.ts: -------------------------------------------------------------------------------- 1 | import { Command, Monitor, MonitorStore } from 'klasa'; 2 | import { Stopwatch } from '@klasa/stopwatch'; 3 | 4 | import type { Message } from '@klasa/core'; 5 | import type { RateLimitToken } from '@klasa/ratelimits'; 6 | 7 | export default class CommandHandler extends Monitor { 8 | 9 | public constructor(store: MonitorStore, directory: string, files: readonly string[]) { 10 | super(store, directory, files, { ignoreOthers: false }); 11 | this.ignoreEdits = !this.client.options.commands.editing; 12 | } 13 | 14 | public async run(message: Message): Promise { 15 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 16 | if (message.guild && !message.guild.me) await message.guild.members.fetch(this.client.user!.id); 17 | if (!message.channel.postable || (!message.commandText && !message.prefix)) return undefined; 18 | await Promise.all([message.guildSettings.sync(), message.author.settings.sync()]); 19 | if (!message.commandText && message.prefix === this.client.mentionPrefix) { 20 | const prefix = message.guildSettings.get('prefix') as string | string[]; 21 | return message.replyLocale('PREFIX_REMINDER', [prefix.length ? prefix : undefined]); 22 | } 23 | if (!message.commandText) return undefined; 24 | if (!message.command) { 25 | this.client.emit('commandUnknown', message, message.commandText, message.prefix, message.prefixLength); 26 | return undefined; 27 | } 28 | this.client.emit('commandRun', message, message.command, message.args); 29 | 30 | return this.runCommand(message); 31 | } 32 | 33 | private async runCommand(message: Message): Promise { 34 | const timer = new Stopwatch(); 35 | if (this.client.options.commands.typing) message.channel.typing.start(); 36 | let token: RateLimitToken | null = null; 37 | try { 38 | const command = message.command as Command; 39 | 40 | if (!this.client.owners.has(message.author) && command.cooldowns.time) { 41 | const ratelimit = command.cooldowns.acquire(message.guild ? Reflect.get(message, command.cooldownLevel).id : message.author.id); 42 | if (ratelimit.limited) throw message.language.get('INHIBITOR_COOLDOWN', Math.ceil(ratelimit.remainingTime / 1000), command.cooldownLevel !== 'author'); 43 | token = ratelimit.take(); 44 | } 45 | 46 | await this.client.inhibitors.run(message, command); 47 | try { 48 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 49 | await message.prompter!.run(); 50 | try { 51 | // Obtain the sub-command name, defaulting to 'run', then retrieve it, check whether or not it's a 52 | // function, and when true, call apply on it with the command context and the arguments. 53 | const subCommandName = command.subcommands ? message.params.shift() as string : 'run'; 54 | const subCommandMethod = Reflect.get(command, subCommandName); 55 | if (typeof subCommandMethod !== 'function') throw new TypeError(`The sub-command ${subCommandName} does not exist for ${command.name}.`); 56 | 57 | const result = Reflect.apply(subCommandMethod, command, [message, message.params]); 58 | timer.stop(); 59 | const response = await result; 60 | 61 | this.client.finalizers.run(message, command, response, timer); 62 | 63 | if (token) token.commit(); 64 | this.client.emit('commandSuccess', message, command, message.params, response); 65 | } catch (error) { 66 | if (token) token.revert(); 67 | this.client.emit('commandError', message, command, message.params, error); 68 | } 69 | } catch (argumentError) { 70 | if (token) token.revert(); 71 | this.client.emit('argumentError', message, command, message.params, argumentError); 72 | } 73 | } catch (response) { 74 | if (token) token.revert(); 75 | this.client.emit('commandInhibited', message, message.command, response); 76 | } finally { 77 | if (this.client.options.commands.typing) message.channel.typing.stop(); 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/serializers/any.ts: -------------------------------------------------------------------------------- 1 | import { Serializer } from 'klasa'; 2 | 3 | export default class CoreSerializer extends Serializer {} 4 | -------------------------------------------------------------------------------- /src/serializers/boolean.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerStore, SerializerUpdateContext } from 'klasa'; 2 | 3 | const truths = ['1', 'true', '+', 't', 'yes', 'y']; 4 | const falses = ['0', 'false', '-', 'f', 'no', 'n']; 5 | 6 | export default class CoreSerializer extends Serializer { 7 | 8 | public constructor(store: SerializerStore, directory: string, file: readonly string[]) { 9 | super(store, directory, file, { aliases: ['bool'] }); 10 | } 11 | 12 | public async validate(data: unknown, { entry, language }: SerializerUpdateContext): Promise { 13 | const boolean = String(data).toLowerCase(); 14 | if (truths.includes(boolean)) return true; 15 | if (falses.includes(boolean)) return false; 16 | throw language.get('RESOLVER_INVALID_BOOL', entry.key); 17 | } 18 | 19 | public stringify(value: boolean): string { 20 | return value ? 'Enabled' : 'Disabled'; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/serializers/channel.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerStore, SerializerUpdateContext, SchemaEntry, Language } from 'klasa'; 2 | import { GuildBasedChannel, Channel, Channels } from '@klasa/core'; 3 | import { ChannelType } from '@klasa/dapi-types'; 4 | 5 | export default class CoreSerializer extends Serializer { 6 | 7 | public constructor(store: SerializerStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['textchannel', 'voicechannel', 'categorychannel', 'storechannel', 'newschannel'] }); 9 | } 10 | 11 | public async validate(data: string | Channels, { entry, language, guild }: SerializerUpdateContext): Promise { 12 | if (data instanceof Channel) return this.checkChannel(data, entry, language); 13 | 14 | const parsed = Serializer.regex.channel.exec(data); 15 | const channel = parsed ? (guild || this.client).channels.get(parsed[1]) : null; 16 | if (channel) return this.checkChannel(channel as Channels, entry, language); 17 | throw language.get('RESOLVER_INVALID_CHANNEL', entry.key); 18 | } 19 | 20 | public serialize(value: GuildBasedChannel): string { 21 | return value.id; 22 | } 23 | 24 | public stringify(value: GuildBasedChannel): string { 25 | return value.name; 26 | } 27 | 28 | private checkChannel(data: Channels, entry: SchemaEntry, language: Language): Channels { 29 | if ( 30 | entry.type === 'channel' || 31 | (entry.type === 'textchannel' && data.type === ChannelType.GuildText) || 32 | (entry.type === 'voicechannel' && data.type === ChannelType.GuildVoice) || 33 | (entry.type === 'categorychannel' && data.type === ChannelType.GuildCategory) || 34 | (entry.type === 'storechannel' && data.type === ChannelType.GuildStore) || 35 | (entry.type === 'newschannel' && data.type === ChannelType.GuildNews) 36 | ) return data; 37 | throw language.get('RESOLVER_INVALID_CHANNEL', entry.key); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/serializers/guild.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerUpdateContext } from 'klasa'; 2 | import { Guild } from '@klasa/core'; 3 | 4 | export default class CoreSerializer extends Serializer { 5 | 6 | public async validate(data: string | Guild, { entry, language }: SerializerUpdateContext): Promise { 7 | if (data instanceof Guild) return data; 8 | const guild = Serializer.regex.snowflake.test(data) ? this.client.guilds.get(data) : null; 9 | if (guild) return guild; 10 | throw language.get('RESOLVER_INVALID_GUILD', entry.key); 11 | } 12 | 13 | public serialize(value: Guild): string { 14 | return value.id; 15 | } 16 | 17 | public stringify(value: Guild): string { 18 | return value.name; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/serializers/number.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerStore, SerializerUpdateContext } from 'klasa'; 2 | 3 | export default class CoreSerializer extends Serializer { 4 | 5 | public constructor(store: SerializerStore, directory: string, file: readonly string[]) { 6 | super(store, directory, file, { aliases: ['integer', 'float'] }); 7 | } 8 | 9 | public async validate(data: string | number, { entry, language }: SerializerUpdateContext): Promise { 10 | let number: number; 11 | switch (entry.type) { 12 | case 'integer': 13 | number = typeof data === 'number' ? data : parseInt(data); 14 | if (Number.isInteger(number)) return number; 15 | throw language.get('RESOLVER_INVALID_INT', entry.key); 16 | case 'number': 17 | case 'float': 18 | number = typeof data === 'number' ? data : parseFloat(data); 19 | if (!Number.isNaN(number)) return number; 20 | throw language.get('RESOLVER_INVALID_FLOAT', entry.key); 21 | } 22 | 23 | // noop 24 | return null; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/serializers/piece.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerUpdateContext, SerializerStore } from 'klasa'; 2 | 3 | import type { Piece } from '@klasa/core'; 4 | 5 | export default class CoreSerializer extends Serializer { 6 | 7 | public constructor(store: SerializerStore, directory: string, file: readonly string[]) { 8 | super(store, directory, file, { aliases: ['command', 'language'] }); 9 | } 10 | 11 | public async validate(data: string | Piece, { entry, language }: SerializerUpdateContext): Promise { 12 | const store = this.client[`${entry.type}s` as 'languages' | 'commands']; 13 | const parsed = typeof data === 'string' ? store.get(data) : data; 14 | if (parsed && parsed instanceof store.Holds) return parsed; 15 | throw language.get('RESOLVER_INVALID_PIECE', entry.key, entry.type); 16 | } 17 | 18 | public serialize(value: Piece): string { 19 | return value.name; 20 | } 21 | 22 | public stringify(value: Piece): string { 23 | return value.name; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/serializers/role.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerUpdateContext } from 'klasa'; 2 | import { Role } from '@klasa/core'; 3 | 4 | export default class CoreSerializer extends Serializer { 5 | 6 | public async validate(data: string | Role, { entry, language, guild }: SerializerUpdateContext): Promise { 7 | if (!guild) throw this.client.languages.default.get('RESOLVER_INVALID_GUILD', entry.key); 8 | if (data instanceof Role) return data; 9 | 10 | const parsed = Serializer.regex.role.exec(data); 11 | const role = parsed ? guild.roles.get(parsed[1]) : guild.roles.findValue(value => value.name === data) || null; 12 | if (role) return role; 13 | throw language.get('RESOLVER_INVALID_ROLE', entry.key); 14 | } 15 | 16 | public serialize(value: Role): string { 17 | return value.id; 18 | } 19 | 20 | public stringify(value: Role): string { 21 | return value.name; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/serializers/string.ts: -------------------------------------------------------------------------------- 1 | import { Serializer } from 'klasa'; 2 | 3 | export default class CoreSerializer extends Serializer { 4 | 5 | public async validate(data: unknown): Promise { 6 | return String(data); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/serializers/url.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerUpdateContext } from 'klasa'; 2 | import { URL } from 'url'; 3 | 4 | export default class CoreSerializer extends Serializer { 5 | 6 | public async validate(data: string | URL, { entry, language }: SerializerUpdateContext): Promise { 7 | const url = data instanceof URL ? data : new URL(data); 8 | if (url.protocol && url.hostname) return url.href; 9 | throw language.get('RESOLVER_INVALID_URL', entry.key); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/serializers/user.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerUpdateContext } from 'klasa'; 2 | 3 | import type { User } from '@klasa/core'; 4 | 5 | export default class CoreSerializer extends Serializer { 6 | 7 | public async validate(data: string | User, { entry, language }: SerializerUpdateContext): Promise { 8 | let user = this.client.users.resolve(data); 9 | if (user) return user; 10 | 11 | const resolved = Serializer.regex.userOrMember.exec(data as string); 12 | if (resolved) user = await this.client.users.fetch(resolved[1]).catch(() => null); 13 | if (user) return user; 14 | throw language.get('RESOLVER_INVALID_USER', entry.key); 15 | } 16 | 17 | public serialize(value: User): string { 18 | return value.id; 19 | } 20 | 21 | public stringify(value: User): string { 22 | return value.tag; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /test/GatewayStore.ts: -------------------------------------------------------------------------------- 1 | import unknownTest, { TestInterface } from 'ava'; 2 | import { Cache } from '@klasa/cache'; 3 | import { createClient } from './lib/SettingsClient'; 4 | import { 5 | Gateway, 6 | GatewayStore, 7 | KlasaClient 8 | } from '../src'; 9 | 10 | const ava = unknownTest as TestInterface<{ 11 | client: KlasaClient 12 | }>; 13 | 14 | ava.beforeEach(async (test): Promise => { 15 | test.context = { 16 | client: createClient() 17 | }; 18 | }); 19 | 20 | ava('GatewayDriver Properties', (test): void => { 21 | test.plan(3); 22 | 23 | const { client } = test.context; 24 | const gatewayDriver = new GatewayStore(client); 25 | 26 | test.true(gatewayDriver instanceof Cache); 27 | test.is(gatewayDriver.client, client); 28 | 29 | // No gateway is registered 30 | test.is(gatewayDriver.size, 0); 31 | }); 32 | 33 | ava('GatewayDriver (From Client)', (test): void => { 34 | test.plan(6); 35 | 36 | const { client } = test.context; 37 | 38 | test.true(client.gateways instanceof Cache); 39 | test.is(client.gateways.client, client); 40 | 41 | // clientStorage, guilds, users 42 | test.is(client.gateways.size, 3); 43 | test.true(client.gateways.get('clientStorage') instanceof Gateway); 44 | test.true(client.gateways.get('guilds') instanceof Gateway); 45 | test.true(client.gateways.get('users') instanceof Gateway); 46 | }); 47 | 48 | ava('GatewayDriver#register', (test): void => { 49 | test.plan(2); 50 | 51 | const client = createClient(); 52 | const gateway = new Gateway(client, 'someCustomGateway'); 53 | 54 | test.is(client.gateways.register(gateway), client.gateways); 55 | test.is(client.gateways.get('someCustomGateway'), gateway); 56 | }); 57 | 58 | ava('GatewayDriver#init', async (test): Promise => { 59 | test.plan(7); 60 | 61 | const client = createClient(); 62 | 63 | test.false((client.gateways.get('guilds') as Gateway).ready); 64 | test.false((client.gateways.get('users') as Gateway).ready); 65 | test.false((client.gateways.get('clientStorage') as Gateway).ready); 66 | 67 | test.is(await client.gateways.init(), undefined); 68 | 69 | test.true((client.gateways.get('guilds') as Gateway).ready); 70 | test.true((client.gateways.get('users') as Gateway).ready); 71 | test.true((client.gateways.get('clientStorage') as Gateway).ready); 72 | }); 73 | 74 | ava('GatewayDriver#toJSON', (test): void => { 75 | const client = createClient(); 76 | test.deepEqual(client.gateways.toJSON(), { 77 | guilds: { 78 | name: 'guilds', 79 | provider: 'json', 80 | schema: {} 81 | }, 82 | users: { 83 | name: 'users', 84 | provider: 'json', 85 | schema: {} 86 | }, 87 | clientStorage: { 88 | name: 'clientStorage', 89 | provider: 'json', 90 | schema: {} 91 | } 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/ProviderStore.ts: -------------------------------------------------------------------------------- 1 | import unknownTest, { TestInterface } from 'ava'; 2 | import { createClient } from './lib/SettingsClient'; 3 | import { 4 | KlasaClient, 5 | Provider, 6 | ProviderStore 7 | } from '../src'; 8 | 9 | const ava = unknownTest as TestInterface<{ 10 | client: KlasaClient 11 | }>; 12 | 13 | ava.beforeEach(async (test): Promise => { 14 | test.context = { 15 | client: createClient() 16 | }; 17 | }); 18 | 19 | ava('ProviderStore Properties', (test): void => { 20 | test.plan(6); 21 | 22 | const { providers } = test.context.client; 23 | 24 | // Test the store's properties 25 | test.true(providers instanceof ProviderStore); 26 | test.is(providers.client, test.context.client); 27 | test.is(providers.Holds, Provider); 28 | test.is(providers.name, 'providers'); 29 | 30 | // Mock provider from tests 31 | test.is(providers.size, 1); 32 | test.true(providers.has('json')); 33 | }); 34 | 35 | ava('ProviderStore#default', (test): void => { 36 | test.plan(2); 37 | 38 | const { providers } = test.context.client; 39 | 40 | test.context.client.options.providers.default = 'json'; 41 | test.is(providers.default, providers.get('json')); 42 | providers.clear(); 43 | test.is(providers.default, null); 44 | }); 45 | 46 | ava('ProviderStore#clear', (test): void => { 47 | test.plan(2); 48 | 49 | const { providers } = test.context.client; 50 | 51 | test.is(providers.size, 1); 52 | providers.clear(); 53 | test.is(providers.size, 0); 54 | }); 55 | 56 | ava('ProviderStore#delete (From Name)', (test): void => { 57 | test.plan(2); 58 | 59 | const { providers } = test.context.client; 60 | 61 | test.true(providers.remove('json')); 62 | test.is(providers.size, 0); 63 | }); 64 | 65 | ava('ProviderStore#delete (From Instance)', (test): void => { 66 | test.plan(2); 67 | 68 | const { providers } = test.context.client; 69 | 70 | test.true(providers.remove(providers.get('json') as Provider)); 71 | test.is(providers.size, 0); 72 | }); 73 | 74 | ava('ProviderStore#delete (Invalid)', (test): void => { 75 | test.plan(2); 76 | 77 | const { providers } = test.context.client; 78 | 79 | test.false(providers.remove('DoesNotExist')); 80 | test.is(providers.size, 1); 81 | }); 82 | -------------------------------------------------------------------------------- /test/Schema.ts: -------------------------------------------------------------------------------- 1 | import ava from 'ava'; 2 | import { Schema, SchemaEntry, Settings } from '../src'; 3 | 4 | ava('Schema Properties', (test): void => { 5 | test.plan(8); 6 | 7 | const schema = new Schema(); 8 | 9 | test.true(schema instanceof Map); 10 | test.is(schema.size, 0); 11 | 12 | test.true(schema.defaults instanceof Settings); 13 | test.is(schema.defaults.size, 0); 14 | 15 | test.deepEqual(schema.toJSON(), {}); 16 | 17 | test.deepEqual([...schema.keys()], []); 18 | test.deepEqual([...schema.values()], []); 19 | test.deepEqual([...schema.entries()], []); 20 | }); 21 | 22 | ava('Schema#add', (test): void => { 23 | test.plan(14); 24 | 25 | const schema = new Schema(); 26 | test.is(schema.add('test', 'String'), schema); 27 | 28 | test.true(schema instanceof Schema, '"add" method must be chainable.'); 29 | 30 | test.is(schema.defaults.size, 1); 31 | const settingsEntry = schema.defaults.get('test'); 32 | test.is(settingsEntry, null); 33 | 34 | test.is(schema.size, 1); 35 | const schemaEntry = schema.get('test') as SchemaEntry; 36 | test.true(schemaEntry instanceof SchemaEntry); 37 | test.is(schemaEntry.key, 'test'); 38 | test.is(schemaEntry.parent, schema); 39 | test.is(schemaEntry.type, 'string'); 40 | test.deepEqual(schemaEntry.toJSON(), { 41 | array: false, 42 | default: null, 43 | inclusive: false, 44 | maximum: null, 45 | minimum: null, 46 | resolve: true, 47 | type: 'string' 48 | }); 49 | 50 | test.deepEqual(schema.toJSON(), { 51 | test: { 52 | array: false, 53 | default: null, 54 | inclusive: false, 55 | maximum: null, 56 | minimum: null, 57 | resolve: true, 58 | type: 'string' 59 | } 60 | }); 61 | 62 | test.deepEqual([...schema.keys()], ['test']); 63 | test.deepEqual([...schema.values()], [schemaEntry]); 64 | test.deepEqual([...schema.entries()], [['test', schemaEntry]]); 65 | }); 66 | 67 | ava('Schema#add (Edit | Entry To Entry)', (test): void => { 68 | test.plan(5); 69 | 70 | const schema = new Schema().add('key', 'String'); 71 | test.is(schema.defaults.get('key'), null); 72 | test.is((schema.get('key') as SchemaEntry).default, null); 73 | 74 | test.is(schema.add('key', 'String', { default: 'Hello' }), schema); 75 | test.is(schema.defaults.get('key'), 'Hello'); 76 | test.is((schema.get('key') as SchemaEntry).default, 'Hello'); 77 | }); 78 | 79 | ava('Schema#add (Ready)', (test): void => { 80 | const schema = new Schema(); 81 | schema.ready = true; 82 | 83 | test.throws(() => schema.add('key', 'String'), { message: 'Cannot modify the schema after being initialized.' }); 84 | }); 85 | 86 | ava('Schema#get (Entry)', (test): void => { 87 | const schema = new Schema().add('key', 'String'); 88 | test.true(schema.get('key') instanceof SchemaEntry); 89 | }); 90 | 91 | ava('Schema#get (Folder From Entry)', (test): void => { 92 | const schema = new Schema().add('key', 'String'); 93 | test.is(schema.get('key.non.existent.path'), undefined); 94 | }); 95 | 96 | ava('Schema#delete', (test): void => { 97 | test.plan(3); 98 | 99 | const schema = new Schema().add('key', 'String'); 100 | test.is(schema.defaults.get('key'), null); 101 | 102 | test.true(schema.delete('key')); 103 | test.is(schema.defaults.get('key'), undefined); 104 | }); 105 | 106 | ava('Schema#delete (Not Exists)', (test): void => { 107 | const schema = new Schema(); 108 | test.false(schema.delete('key')); 109 | }); 110 | 111 | ava('Schema#delete (Ready)', (test): void => { 112 | const schema = new Schema(); 113 | schema.ready = true; 114 | 115 | test.throws(() => schema.delete('key'), { message: 'Cannot modify the schema after being initialized.' }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/lib/MockLanguage.ts: -------------------------------------------------------------------------------- 1 | import { Language, SchemaEntry } from '../../src'; 2 | 3 | export class MockLanguage extends Language { 4 | 5 | public language = { 6 | DEFAULT: (key: string, ...args: unknown[]): string => `[DEFAULT]: ${key} ${args.join(' ')}`, 7 | SETTING_GATEWAY_KEY_NOEXT: (key: string): string => `[SETTING_GATEWAY_KEY_NOEXT]: ${key}`, 8 | SETTING_GATEWAY_CHOOSE_KEY: (keys: string[]): string => `[SETTING_GATEWAY_CHOOSE_KEY]: ${keys.join(' ')}`, 9 | SETTING_GATEWAY_MISSING_VALUE: (entry: SchemaEntry, value: string): string => `[SETTING_GATEWAY_MISSING_VALUE]: ${entry.key} ${value}`, 10 | SETTING_GATEWAY_DUPLICATE_VALUE: (entry: SchemaEntry, value: string): string => `[SETTING_GATEWAY_DUPLICATE_VALUE]: ${entry.key} ${value}`, 11 | SETTING_GATEWAY_INVALID_FILTERED_VALUE: (entry: SchemaEntry, value: unknown): string => `[SETTING_GATEWAY_INVALID_FILTERED_VALUE]: ${entry.key} ${value}`, 12 | RESOLVER_MINMAX_EXACTLY: (key: string, value: number, inclusive: boolean): string => `[RESOLVER_MINMAX_EXACTLY]: ${key} ${value} ${inclusive}`, 13 | RESOLVER_MINMAX_BOTH: (key: string, minimum: number, maximum: number, inclusive: boolean): string => `[RESOLVER_MINMAX_BOTH]: ${key} ${minimum} ${maximum} ${inclusive}`, 14 | RESOLVER_MINMAX_MIN: (key: string, minimum: number, inclusive: number): string => `[RESOLVER_MINMAX_MIN]: ${key} ${minimum} ${inclusive}`, 15 | RESOLVER_MINMAX_MAX: (key: string, maximum: number, inclusive: number): string => `[RESOLVER_MINMAX_MAX]: ${key} ${maximum} ${inclusive}` 16 | }; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /test/lib/MockNumberSerializer.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerStore, SerializerUpdateContext } from '../../src'; 2 | 3 | export class MockNumberSerializer extends Serializer { 4 | 5 | public constructor(store: SerializerStore, directory: string, file: string[]) { 6 | super(store, directory, file, { name: 'number', aliases: ['integer', 'float'] }); 7 | } 8 | 9 | public deserialize(data: unknown): number { 10 | return Number(data); 11 | } 12 | 13 | public validate(data: unknown, { entry, language }: SerializerUpdateContext): number | null { 14 | let parsed: number; 15 | switch (entry.type) { 16 | case 'integer': 17 | parsed = parseInt(data as string); 18 | if (Number.isInteger(parsed) && Serializer.minOrMax(parsed, entry, language)) return parsed; 19 | throw language.get('RESOLVER_INVALID_INT', entry.key); 20 | case 'number': 21 | case 'float': 22 | parsed = parseFloat(data as string); 23 | if (!isNaN(parsed) && Serializer.minOrMax(parsed, entry, language)) return parsed; 24 | throw language.get('RESOLVER_INVALID_FLOAT', entry.key); 25 | } 26 | // noop 27 | return null; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /test/lib/MockObjectSerializer.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerStore } from '../../src'; 2 | 3 | export class MockObjectSerializer extends Serializer { 4 | 5 | public constructor(store: SerializerStore, directory: string, file: string[]) { 6 | super(store, directory, file, { name: 'object' }); 7 | } 8 | 9 | public resolve(data: unknown): unknown { 10 | return data === null ? null : { data }; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /test/lib/MockProvider.ts: -------------------------------------------------------------------------------- 1 | import { Provider, SettingsUpdateResults } from '../../src'; 2 | import { mergeObjects } from '@klasa/utils'; 3 | 4 | export class MockProvider extends Provider { 5 | 6 | private tables = new Map>(); 7 | 8 | public async createTable(table: string): Promise { 9 | if (this.tables.has(table)) throw new Error('Table Exists'); 10 | this.tables.set(table, new Map()); 11 | } 12 | 13 | public async deleteTable(table: string): Promise { 14 | if (!this.tables.has(table)) throw new Error('Table Not Exists'); 15 | this.tables.delete(table); 16 | } 17 | 18 | public async hasTable(table: string): Promise { 19 | return this.tables.has(table); 20 | } 21 | 22 | public async create(table: string, entry: string, data: unknown | SettingsUpdateResults): Promise { 23 | const resolvedTable = this.tables.get(table); 24 | if (typeof resolvedTable === 'undefined') throw new Error('Table Not Exists'); 25 | if (resolvedTable.has(entry)) throw new Error('Entry Exists'); 26 | resolvedTable.set(entry, { ...this.parseUpdateInput(data), id: entry }); 27 | } 28 | 29 | public async delete(table: string, entry: string): Promise { 30 | const resolvedTable = this.tables.get(table); 31 | if (typeof resolvedTable === 'undefined') throw new Error('Table Not Exists'); 32 | if (!resolvedTable.has(entry)) throw new Error('Entry Not Exists'); 33 | resolvedTable.delete(entry); 34 | } 35 | 36 | public async get(table: string, entry: string): Promise { 37 | const resolvedTable = this.tables.get(table); 38 | if (typeof resolvedTable === 'undefined') throw new Error('Table Not Exists'); 39 | return resolvedTable.get(entry) || null; 40 | } 41 | 42 | public async getAll(table: string, entries?: readonly string[]): Promise { 43 | const resolvedTable = this.tables.get(table); 44 | if (typeof resolvedTable === 'undefined') throw new Error('Table Not Exists'); 45 | 46 | if (typeof entries === 'undefined') { 47 | return [...resolvedTable.values()]; 48 | } 49 | 50 | const values: unknown[] = []; 51 | for (const [key, value] of resolvedTable.entries()) { 52 | if (entries.includes(key)) values.push(value); 53 | } 54 | 55 | return values; 56 | } 57 | 58 | public async getKeys(table: string): Promise { 59 | const resolvedTable = this.tables.get(table); 60 | if (typeof resolvedTable === 'undefined') throw new Error('Table Not Exists'); 61 | return [...resolvedTable.keys()]; 62 | } 63 | 64 | public async has(table: string, entry: string): Promise { 65 | const resolvedTable = this.tables.get(table); 66 | if (typeof resolvedTable === 'undefined') throw new Error('Table Not Exists'); 67 | return resolvedTable.has(entry); 68 | } 69 | 70 | public async update(table: string, entry: string, data: unknown | SettingsUpdateResults): Promise { 71 | const resolvedTable = this.tables.get(table); 72 | if (typeof resolvedTable === 'undefined') throw new Error('Table Not Exists'); 73 | 74 | const resolvedEntry = resolvedTable.get(entry); 75 | if (typeof resolvedEntry === 'undefined') throw new Error('Entry Not Exists'); 76 | 77 | resolvedTable.set(entry, mergeObjects({ ...resolvedEntry as Record }, this.parseUpdateInput(data))); 78 | } 79 | 80 | public async replace(table: string, entry: string, data: unknown | SettingsUpdateResults): Promise { 81 | const resolvedTable = this.tables.get(table); 82 | if (typeof resolvedTable === 'undefined') throw new Error('Table Not Exists'); 83 | 84 | const resolvedEntry = resolvedTable.get(entry); 85 | if (typeof resolvedEntry === 'undefined') throw new Error('Entry Not Exists'); 86 | 87 | resolvedTable.set(entry, { ...this.parseUpdateInput(data), id: entry }); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /test/lib/MockStringSerializer.ts: -------------------------------------------------------------------------------- 1 | import { Serializer, SerializerStore, SerializerUpdateContext } from '../../src'; 2 | 3 | export class MockStringSerializer extends Serializer { 4 | 5 | public constructor(store: SerializerStore, directory: string, file: string[]) { 6 | super(store, directory, file, { name: 'string' }); 7 | } 8 | 9 | public deserialize(data: unknown): string { 10 | return String(data); 11 | } 12 | 13 | public validate(data: unknown, { entry, language }: SerializerUpdateContext): string | null { 14 | const parsed = String(data); 15 | return Serializer.minOrMax(parsed.length, entry, language) ? parsed : null; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /test/lib/SettingsClient.ts: -------------------------------------------------------------------------------- 1 | import { KlasaClient, KlasaClientOptions, Schema } from '../../src'; 2 | import { MockProvider } from './MockProvider'; 3 | import { MockLanguage } from './MockLanguage'; 4 | import { MockNumberSerializer } from './MockNumberSerializer'; 5 | import { MockObjectSerializer } from './MockObjectSerializer'; 6 | import { MockStringSerializer } from './MockStringSerializer'; 7 | 8 | export function createClient(options: Partial = {}): KlasaClient { 9 | const client = new KlasaClient({ settings: { 10 | gateways: { 11 | clientStorage: { 12 | schema: (schema): Schema => schema 13 | }, 14 | users: { 15 | schema: (schema): Schema => schema 16 | }, 17 | guilds: { 18 | schema: (schema): Schema => schema 19 | } 20 | }, 21 | preserve: true 22 | }, ...options }); 23 | for (const gateway of client.gateways.values()) { 24 | gateway.schema.clear(); 25 | gateway.schema.defaults.clear(); 26 | } 27 | 28 | Map.prototype.set.call(client.providers, 'json', new MockProvider(client.providers, 'providers', ['json.js'], { name: 'json' })); 29 | Map.prototype.set.call(client.languages, 'en-US', new MockLanguage(client.languages, 'languages', ['en-US.js'])); 30 | Map.prototype.set.call(client.serializers, 'number', new MockNumberSerializer(client.serializers, 'serializers', ['number.js'])); 31 | Map.prototype.set.call(client.serializers, 'object', new MockObjectSerializer(client.serializers, 'serializers', ['object.js'])); 32 | Map.prototype.set.call(client.serializers, 'string', new MockStringSerializer(client.serializers, 'serializers', ['string.js'])); 33 | return client; 34 | } 35 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import ava from 'ava'; 2 | 3 | ava('sample test', (test): void => { 4 | test.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "strict": true, 6 | "lib": ["ESNext"], 7 | "module": "commonjs", 8 | "noUnusedParameters": true, 9 | "outDir": "./dist", 10 | "sourceMap": true, 11 | "types": ["node", "ws", "node-fetch"], 12 | "declaration": true, 13 | "noUnusedLocals": true, 14 | "removeComments": false, 15 | "importsNotUsedAsValues": "error", 16 | "target": "ES2019", 17 | "incremental": true, 18 | "resolveJsonModule": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "klasa": ["src"] 22 | } 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "bin", 27 | "examples", 28 | "scripts" 29 | ], 30 | "include": [ 31 | "./src/**/*", 32 | "./test/**/*" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "inputFiles": ["./src"], 3 | "mode": "modules", 4 | "json": "./docs.json" 5 | } --------------------------------------------------------------------------------