├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── codequality.yml │ └── test.yml ├── .gitignore ├── .nycrc.json ├── LICENSE ├── README.md ├── package.json ├── src ├── actions │ ├── INVITE_CREATE.ts │ ├── INVITE_DELETE.ts │ ├── PRESENCE_UPDATE.ts │ ├── READY.ts │ ├── RESUMED.ts │ ├── TYPING_START.ts │ ├── USER_UPDATE.ts │ ├── VOICE_SERVER_UPDATE.ts │ ├── VOICE_STATE_UPDATE.ts │ ├── WEBHOOKS_UPDATE.ts │ ├── channels │ │ ├── CHANNEL_CREATE.ts │ │ ├── CHANNEL_DELETE.ts │ │ ├── CHANNEL_PINS_UPDATE.ts │ │ └── CHANNEL_UPDATE.ts │ ├── guilds │ │ ├── GUILD_CREATE.ts │ │ ├── GUILD_DELETE.ts │ │ ├── GUILD_EMOJIS_UPDATE.ts │ │ ├── GUILD_INTEGRATIONS_UPDATE.ts │ │ ├── GUILD_UPDATE.ts │ │ ├── bans │ │ │ ├── GUILD_BAN_ADD.ts │ │ │ └── GUILD_BAN_REMOVE.ts │ │ ├── members │ │ │ ├── GUILD_MEMBERS_CHUNK.ts │ │ │ ├── GUILD_MEMBER_ADD.ts │ │ │ ├── GUILD_MEMBER_REMOVE.ts │ │ │ └── GUILD_MEMBER_UPDATE.ts │ │ └── roles │ │ │ ├── GUILD_ROLE_CREATE.ts │ │ │ ├── GUILD_ROLE_DELETE.ts │ │ │ └── GUILD_ROLE_UPDATE.ts │ └── messages │ │ ├── MESSAGE_CREATE.ts │ │ ├── MESSAGE_DELETE.ts │ │ ├── MESSAGE_DELETE_BULK.ts │ │ ├── MESSAGE_REACTION_ADD.ts │ │ ├── MESSAGE_REACTION_REMOVE.ts │ │ ├── MESSAGE_REACTION_REMOVE_ALL.ts │ │ ├── MESSAGE_REACTION_REMOVE_EMOJI.ts │ │ └── MESSAGE_UPDATE.ts ├── index.ts └── lib │ ├── caching │ ├── stores │ │ ├── BanStore.ts │ │ ├── ChannelPinsStore.ts │ │ ├── ChannelStore.ts │ │ ├── DMChannelStore.ts │ │ ├── GuildChannelInviteStore.ts │ │ ├── GuildChannelStore.ts │ │ ├── GuildEmojiStore.ts │ │ ├── GuildInviteStore.ts │ │ ├── GuildMemberRoleStore.ts │ │ ├── GuildMemberStore.ts │ │ ├── GuildStore.ts │ │ ├── IntegrationStore.ts │ │ ├── InviteStore.ts │ │ ├── MessageReactionStore.ts │ │ ├── MessageReactionUserStore.ts │ │ ├── MessageStore.ts │ │ ├── OverwriteStore.ts │ │ ├── PresenceStore.ts │ │ ├── RoleStore.ts │ │ ├── UserStore.ts │ │ ├── VoiceStateStore.ts │ │ └── base │ │ │ └── DataStore.ts │ └── structures │ │ ├── Attachment.ts │ │ ├── ClientUser.ts │ │ ├── Embed.ts │ │ ├── Invite.ts │ │ ├── Typing.ts │ │ ├── User.ts │ │ ├── Webhook.ts │ │ ├── base │ │ └── Structure.ts │ │ ├── channels │ │ ├── CategoryChannel.ts │ │ ├── Channel.ts │ │ ├── DMChannel.ts │ │ ├── GuildChannel.ts │ │ ├── GuildTextChannel.ts │ │ ├── NewsChannel.ts │ │ ├── StoreChannel.ts │ │ ├── TextChannel.ts │ │ └── VoiceChannel.ts │ │ ├── guilds │ │ ├── AuditLog.ts │ │ ├── AuditLogEntry.ts │ │ ├── Ban.ts │ │ ├── Guild.ts │ │ ├── GuildEmoji.ts │ │ ├── GuildMember.ts │ │ ├── GuildWidget.ts │ │ ├── Integration.ts │ │ ├── Overwrite.ts │ │ ├── Presence.ts │ │ ├── Role.ts │ │ └── VoiceState.ts │ │ ├── messages │ │ ├── Message.ts │ │ ├── MessageAttachment.ts │ │ ├── MessageBuilder.ts │ │ ├── MessageMentions.ts │ │ ├── WebhookMessage.ts │ │ ├── WebhookMessageBuilder.ts │ │ └── reactions │ │ │ ├── MessageReaction.ts │ │ │ └── MessageReactionEmoji.ts │ │ ├── oauth │ │ ├── Application.ts │ │ ├── Team.ts │ │ └── TeamMember.ts │ │ └── presences │ │ ├── ClientPresence.ts │ │ ├── PresenceBuilder.ts │ │ └── PresenceGameBuilder.ts │ ├── client │ ├── BaseClient.ts │ ├── Client.ts │ └── WebhookClient.ts │ ├── pieces │ ├── Action.ts │ ├── ActionStore.ts │ ├── Event.ts │ ├── EventStore.ts │ └── base │ │ ├── AliasPiece.ts │ │ ├── AliasStore.ts │ │ ├── Piece.ts │ │ └── Store.ts │ └── util │ ├── Constants.ts │ ├── Extender.ts │ ├── ImageUtil.ts │ ├── Util.ts │ ├── bitfields │ ├── Activity.ts │ ├── MessageFlags.ts │ ├── Permissions.ts │ ├── Speaking.ts │ └── UserFlags.ts │ ├── collectors │ ├── MessageCollector.ts │ ├── ReactionCollector.ts │ └── base │ │ └── StructureCollector.ts │ └── iterators │ ├── MessageIterator.ts │ └── ReactionIterator.ts ├── test ├── Application.ts ├── BaseClient.ts ├── Team.ts └── WebhookClient.ts ├── tsconfig.json ├── typedoc.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "klasa/eslint-ts" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.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/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 | 33 | Typescript: 34 | name: Typescript 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout Project 38 | uses: actions/checkout@v1 39 | - name: Use Node.js 12 40 | uses: actions/setup-node@v1 41 | with: 42 | node-version: 12 43 | - name: Restore CI Cache 44 | uses: actions/cache@v1 45 | with: 46 | path: node_modules 47 | key: ${{ runner.os }}-12-${{ hashFiles('**/yarn.lock') }} 48 | - name: Install Dependencies 49 | run: yarn 50 | - name: Run TSC 51 | uses: icrawl/action-tsc@v1 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | TypeDocs: 56 | name: TypeDocs 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout Project 60 | uses: actions/checkout@v1 61 | - name: Use Node.js 12 62 | uses: actions/setup-node@v1 63 | with: 64 | node-version: 12 65 | - name: Restore CI Cache 66 | uses: actions/cache@v1 67 | with: 68 | path: node_modules 69 | key: ${{ runner.os }}-12-${{ hashFiles('**/yarn.lock') }} 70 | - name: Install Dependencies 71 | run: yarn 72 | - name: Test Docs 73 | if: github.event_name == 'pull_request' 74 | run: yarn docs 75 | - name: Publish Docs 76 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 77 | run: | 78 | #!/bin/bash 79 | set -euxo pipefail 80 | echo -e "\n# Initialize some useful variables" 81 | REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 82 | BRANCH_OR_TAG=`awk -F/ '{print $2}' <<< $GITHUB_REF` 83 | CURRENT_BRANCH=`awk -F/ '{print $NF}' <<< $GITHUB_REF` 84 | if [ "$BRANCH_OR_TAG" == "heads" ]; then 85 | SOURCE_TYPE="branch" 86 | else 87 | SOURCE_TYPE="tag" 88 | fi 89 | echo -e "\n# Checkout the repo in the target branch" 90 | TARGET_BRANCH="docs" 91 | git clone $REPO out -b $TARGET_BRANCH 92 | yarn docs 93 | echo -e "\n# Move the generated docs to the newly-checked-out repo, to be committed and pushed" 94 | mv docs.json out/${CURRENT_BRANCH//\//_}.json 95 | echo -e "\n# Commit and push" 96 | cd out 97 | git add --all . 98 | git config user.name "${GITHUB_ACTOR}" 99 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 100 | git commit -m "Docs build: ${GITHUB_SHA}" || true 101 | git push origin $TARGET_BRANCH 102 | env: 103 | GITHUB_TOKEN: ${{ secrets.COMMIT_TOKEN }} 104 | -------------------------------------------------------------------------------- /.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 | docs.json 17 | -------------------------------------------------------------------------------- /.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) 2018-2020 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @klasa/core 2 | This is an early alpha discord library which will be the future core of the Klasa Bot framework. This implements a high-level stateful interface over @klasa/rest and @klasa/ws. You are bound to come across missing or broken code/types using this alpha library. (I even found some unintended types while writing this!) Please make issues and pull requests to further the development. 3 | 4 | Simple ping client in typescript 5 | ```typescript 6 | import { Client, ClientEvents, Message } from '@klasa/core'; 7 | import * as config from './config.json'; 8 | 9 | const client = new Client() 10 | .on(ClientEvents.MessageCreate, async (message: Message): Promise => { 11 | if (message.author.bot) return; 12 | if (message.content.toLowerCase().startsWith('ping')) { 13 | const [response] = await message.channel.send(mb => mb.setContent('ping?')); 14 | await response.edit(mb => mb.setContent(`Pong! Took: ${response.createdTimestamp - message.createdTimestamp}ms`)); 15 | } 16 | }); 17 | 18 | client.token = config.token; 19 | 20 | client.connect(); 21 | ``` 22 | 23 | Simple ping client in javascript 24 | ```javascript 25 | const { Client } = require('@klasa/core'); 26 | const { token } = require('./config.json'); 27 | 28 | const client = new Client() 29 | .on('messageCreate', async (message) => { 30 | if (message.author.bot) return; 31 | if (message.content.toLowerCase().startsWith('ping')) { 32 | const [response] = await message.channel.send(mb => mb.setContent('ping?')); 33 | return response.edit(mb => mb.setContent(`Pong! Took: ${response.createdTimestamp - message.createdTimestamp}ms`)); 34 | } 35 | }); 36 | 37 | client.token = token; 38 | 39 | client.connect(); 40 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@klasa/core", 3 | "version": "0.0.3", 4 | "description": "wip-concept", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "scripts": { 8 | "prepublishOnly": "yarn build", 9 | "build": "tsc", 10 | "test": "ava --timeout=2m", 11 | "test:lint": "eslint --ext ts src test", 12 | "test:coverage": "npx nyc check-coverage --lines 90 --functions 90 --branches 90", 13 | "coverage": "npx nyc --require source-map-support/register npm test", 14 | "coverage:report": "npx nyc report --reporter=html", 15 | "lint": "eslint --fix --ext ts src test", 16 | "docs": "typedoc", 17 | "docs:html": "typedoc --inputFiles src --mode file --out docs" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/dirigeants/core.git" 22 | }, 23 | "devDependencies": { 24 | "@ava/typescript": "^1.1.1", 25 | "@typescript-eslint/eslint-plugin": "^4.29.0", 26 | "@typescript-eslint/parser": "^3.10.1", 27 | "ava": "^3.15.0", 28 | "eslint": "^7.32.0", 29 | "eslint-config-klasa": "dirigeants/klasa-lint", 30 | "nock": "^13.1.1", 31 | "nyc": "^15.1.0", 32 | "source-map-support": "^0.5.19", 33 | "typedoc": "^0.19.2", 34 | "typescript": "^4.3.5" 35 | }, 36 | "author": "dirigeants", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/dirigeants/core/issues" 40 | }, 41 | "homepage": "https://github.com/dirigeants/core#readme", 42 | "dependencies": { 43 | "@klasa/bitfield": "^0.0.4", 44 | "@klasa/cache": "^0.0.3", 45 | "@klasa/dapi-types": "^0.3.0", 46 | "@klasa/event-iterator": "^0.0.11", 47 | "@klasa/rest": "^0.5.4", 48 | "@klasa/snowflake": "^0.0.1", 49 | "@klasa/timer-manager": "^0.0.1", 50 | "@klasa/utils": "^0.1.0", 51 | "@klasa/ws": "^0.0.14", 52 | "@types/node": "^16.4.3", 53 | "@types/node-fetch": "^2.5.8", 54 | "fs-nextra": "^0.5.1" 55 | }, 56 | "ava": { 57 | "files": [ 58 | "test/**/*.ts", 59 | "!test/lib" 60 | ], 61 | "typescript": { 62 | "extensions": [ 63 | "ts" 64 | ], 65 | "rewritePaths": { 66 | "test/": "dist/test/" 67 | } 68 | } 69 | }, 70 | "files": [ 71 | "dist/src" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /src/actions/INVITE_CREATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, Invite } from '@klasa/core'; 2 | 3 | import type { InviteCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(): null { 8 | return null; 9 | } 10 | 11 | public build(data: InviteCreateDispatch): Invite { 12 | const guild = data.d.guild_id ? this.client.guilds.get(data.d.guild_id) : null; 13 | const channel = this.client.channels.get(data.d.channel_id); 14 | return new (extender.get('Invite'))(this.client, data, channel, guild); 15 | } 16 | 17 | public cache(data: Invite): void { 18 | if (this.client.options.cache.enabled) { 19 | this.client.invites.set(data.id, data); 20 | if (data.guild) data.guild.invites.set(data.id, data); 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/INVITE_DELETE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, Invite } from '@klasa/core'; 2 | 3 | import type { InviteDeleteDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(): null { 8 | return null; 9 | } 10 | 11 | public build(data: InviteDeleteDispatch): Invite | null { 12 | const guild = data.d.guild_id ? this.client.guilds.get(data.d.guild_id) : null; 13 | const channel = (guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id)) ?? { id: data.d.channel_id }; 14 | return new (extender.get('Invite'))(this.client, { ...data, guild, channel }); 15 | } 16 | 17 | public cache(data: Invite): void { 18 | this.client.invites.delete(data.id); 19 | if (data.guild) data.guild.invites.delete(data.id); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/actions/PRESENCE_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, User, Guild } from '@klasa/core'; 2 | 3 | import type { PresenceUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: PresenceUpdateDispatch): void { 13 | const user = this.acquireUser(data); 14 | if (!user) return; 15 | 16 | const guild = this.getGuild(data); 17 | if (!guild) return; 18 | 19 | const previousPresence = guild.presences.get(user.id)?.clone() ?? null; 20 | this.ensureMember(data, guild, user); 21 | 22 | // eslint-disable-next-line dot-notation 23 | const presence = guild.presences['_add'](data.d); 24 | 25 | this.client.emit(this.clientEvent, presence, previousPresence); 26 | } 27 | 28 | public check(): null { 29 | return null; 30 | } 31 | 32 | public build(): null { 33 | return null; 34 | } 35 | 36 | public cache(): void { 37 | // noop 38 | } 39 | 40 | private acquireUser(data: PresenceUpdateDispatch): User | null { 41 | const user = this.client.users.get(data.d.user.id); 42 | if (user) return user; 43 | // eslint-disable-next-line dot-notation 44 | return data.d.user.username ? this.client.users['_add'](data.d.user) : null; 45 | } 46 | 47 | private getGuild(data: PresenceUpdateDispatch): Guild | null { 48 | return data.d.guild_id ? this.client.guilds.get(data.d.guild_id) ?? null : null; 49 | } 50 | 51 | private ensureMember(data: PresenceUpdateDispatch, guild: Guild, user: User): void { 52 | if (data.d.status && data.d.status !== 'offline') { 53 | // eslint-disable-next-line dot-notation 54 | guild.members['_add']({ user, roles: data.d.roles as string[], deaf: false, mute: false }); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/actions/READY.ts: -------------------------------------------------------------------------------- 1 | import { Action, ClientEvents, extender } from '@klasa/core'; 2 | 3 | import type { ReadyDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: ReadyDispatch): void { 13 | for (const guild of data.d.guilds) { 14 | // If we don't already have this guild cached on READY, and the cache is enabled 15 | // cache it now to prevent emitting a GUILD_CREATE event later when we get 16 | // the full object 17 | if (this.client.options.cache.enabled && !this.client.guilds.has(guild.id)) { 18 | // eslint-disable-next-line dot-notation 19 | const created = new (extender.get('Guild'))(this.client, guild, data.shard_id); 20 | this.client.guilds.set(created.id, created); 21 | } 22 | } 23 | 24 | const ClientUser = extender.get('ClientUser'); 25 | 26 | this.client.user = new ClientUser(this.client, data.d.user); 27 | this.client.users.set(this.client.user.id, this.client.user); 28 | 29 | const shard = this.client.ws.shards.get(data.shard_id); 30 | if (shard) this.client.emit(ClientEvents.ShardReady, shard); 31 | } 32 | 33 | public check(): null { 34 | return null; 35 | } 36 | 37 | public build(): null { 38 | return null; 39 | } 40 | 41 | public cache(): void { 42 | // noop 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/actions/RESUMED.ts: -------------------------------------------------------------------------------- 1 | import { Action, ClientEvents } from '@klasa/core'; 2 | 3 | import type { ResumedDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: ResumedDispatch): void { 13 | const shard = this.client.ws.shards.get(data.shard_id); 14 | this.client.emit(ClientEvents.ShardResumed, shard); 15 | } 16 | 17 | public check(): null { 18 | return null; 19 | } 20 | 21 | public build(): null { 22 | return null; 23 | } 24 | 25 | public cache(): void { 26 | // noop 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/actions/TYPING_START.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@klasa/core'; 2 | 3 | import type { TypingStartDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: TypingStartDispatch): void { 13 | const guild = (data.d.guild_id && this.client.guilds.get(data.d.guild_id)) ?? null; 14 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 15 | if (!channel) return; 16 | 17 | // eslint-disable-next-line dot-notation 18 | if (guild && data.d.member) guild.members['_add'](data.d.member); 19 | 20 | const user = this.client.users.get(data.d.user_id); 21 | if (!user) return; 22 | 23 | this.client.emit(this.clientEvent, channel, user); 24 | } 25 | 26 | public check(): null { 27 | return null; 28 | } 29 | 30 | public build(): null { 31 | return null; 32 | } 33 | 34 | public cache(): void { 35 | // noop 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/actions/USER_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, User } from '@klasa/core'; 2 | 3 | import type { UserUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: UserUpdateDispatch): User | null { 8 | return this.client.users.get(data.d.id) ?? null; 9 | } 10 | 11 | public build(data: UserUpdateDispatch): User { 12 | return new (extender.get('User'))(this.client, data.d); 13 | } 14 | 15 | public cache(data: User): void { 16 | if (this.client.options.cache.enabled) { 17 | this.client.users.set(data.id, data); 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/actions/VOICE_SERVER_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@klasa/core'; 2 | 3 | import type { VoiceServerUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: VoiceServerUpdateDispatch): void { 13 | this.client.emit(this.clientEvent, data); 14 | } 15 | 16 | public check(): null { 17 | return null; 18 | } 19 | 20 | public build(): null { 21 | return null; 22 | } 23 | 24 | public cache(): void { 25 | // noop 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/actions/VOICE_STATE_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, VoiceState } from '@klasa/core'; 2 | 3 | import type { VoiceStateUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: VoiceStateUpdateDispatch): VoiceState | null { 8 | return this.client.guilds.get(data.d.guild_id as string)?.voiceStates.get(data.d.user_id) ?? null; 9 | } 10 | 11 | public build(data: VoiceStateUpdateDispatch): VoiceState | null { 12 | const guild = this.client.guilds.get(data.d.guild_id as string); 13 | return guild ? new (extender.get('VoiceState'))(this.client, data.d, guild) : null; 14 | } 15 | 16 | public cache(data: VoiceState): void { 17 | if (this.client.options.cache.enabled && data.guild) { 18 | data.guild.voiceStates.set(data.id, data); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/actions/WEBHOOKS_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, TextChannel, NewsChannel } from '@klasa/core'; 2 | 3 | import type { WebhooksUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: WebhooksUpdateDispatch): TextChannel | NewsChannel | null { 8 | return this.client.guilds.get(data.d.guild_id)?.channels.get(data.d.channel_id) as TextChannel | NewsChannel ?? null; 9 | } 10 | 11 | public build(): null { 12 | return null; 13 | } 14 | 15 | public cache(): void { 16 | // noop 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/actions/channels/CHANNEL_CREATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, Channel, isGuildChannel, GuildBasedChannel, DMChannel } from '@klasa/core'; 2 | 3 | import type { ChannelCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(): null { 8 | return null; 9 | } 10 | 11 | public build(data: ChannelCreateDispatch): GuildBasedChannel | DMChannel | null { 12 | return Channel.create(this.client, data.d, data.d.guild_id && this.client.guilds.get(data.d.guild_id)) as GuildBasedChannel | DMChannel | null; 13 | } 14 | 15 | public cache(data: GuildBasedChannel | DMChannel): void { 16 | if (this.client.options.cache.enabled) { 17 | this.client.channels.set(data.id, data); 18 | if (isGuildChannel(data)) { 19 | if (data.guild) data.guild.channels.set(data.id, data); 20 | } else { 21 | this.client.dms.set(data.id, data); 22 | } 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/actions/channels/CHANNEL_DELETE.ts: -------------------------------------------------------------------------------- 1 | import { Action, isGuildChannel, GuildBasedChannel, DMChannel } from '@klasa/core'; 2 | 3 | import type { ChannelCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: ChannelCreateDispatch): GuildBasedChannel | DMChannel | null { 8 | const guild = data.d.guild_id ? this.client.guilds.get(data.d.guild_id) : undefined; 9 | return (guild ? guild.channels.get(data.d.id) : this.client.dms.get(data.d.id)) ?? null; 10 | } 11 | 12 | public build(): null { 13 | return null; 14 | } 15 | 16 | public cache(data: GuildBasedChannel | DMChannel): void { 17 | data.deleted = true; 18 | this.client.channels.delete(data.id); 19 | if (isGuildChannel(data)) { 20 | if (data.guild) data.guild.channels.delete(data.id); 21 | } else { 22 | this.client.dms.delete(data.id); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/actions/channels/CHANNEL_PINS_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, isTextBasedChannel } from '@klasa/core'; 2 | 3 | import type { ChannelPinsUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: ChannelPinsUpdateDispatch): void { 13 | const guild = (data.d.guild_id && this.client.guilds.get(data.d.guild_id)) ?? null; 14 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 15 | if (!channel || !isTextBasedChannel(channel)) return; 16 | 17 | this.client.emit(this.clientEvent, channel, this.parseDate(data.d.last_pin_timestamp)); 18 | } 19 | 20 | public check(): null { 21 | return null; 22 | } 23 | 24 | public build(): null { 25 | return null; 26 | } 27 | 28 | public cache(): void { 29 | // noop 30 | } 31 | 32 | private parseDate(date: string | undefined): Date | null { 33 | if (!date) return null; 34 | 35 | const parsed = new Date(date); 36 | return Number.isNaN(parsed.getTime()) ? null : parsed; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/actions/channels/CHANNEL_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, Channel, isGuildChannel, DMChannel } from '@klasa/core'; 2 | 3 | import type { ChannelCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: ChannelCreateDispatch): Channel | null { 8 | const guild = data.d.guild_id ? this.client.guilds.get(data.d.guild_id) : undefined; 9 | return (guild ? guild.channels.get(data.d.id) : this.client.dms.get(data.d.id)) ?? null; 10 | } 11 | 12 | public build(data: ChannelCreateDispatch): Channel | null { 13 | return Channel.create(this.client, data.d); 14 | } 15 | 16 | public cache(data: Channel): void { 17 | if (this.client.options.cache.enabled) { 18 | this.client.channels.set(data.id, data); 19 | if (isGuildChannel(data)) { 20 | if (data.guild) data.guild.channels.set(data.id, data); 21 | } else { 22 | this.client.dms.set(data.id, data as DMChannel); 23 | } 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/actions/guilds/GUILD_CREATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, ClientEvents, extender } from '@klasa/core'; 2 | 3 | import type { GuildCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: GuildCreateDispatch): void { 13 | const guild = this.client.guilds.get(data.d.id); 14 | // If the guild was not in cached, this is a new guild, emit GuildCreate 15 | if (!guild) { 16 | const created = new (extender.get('Guild'))(this.client, data.d, data.shard_id); 17 | if (this.client.options.cache.enabled) { 18 | this.client.guilds.set(created.id, created); 19 | } 20 | 21 | this.client.emit(ClientEvents.GuildCreate, created); 22 | return; 23 | } 24 | 25 | const { unavailable } = guild; 26 | 27 | // eslint-disable-next-line dot-notation 28 | guild['_patch'](data.d); 29 | 30 | // If it was unavailable and switches to available, emit GuildAvailable 31 | if (unavailable && !guild.unavailable) { 32 | this.client.emit(ClientEvents.GuildAvailable, guild); 33 | } 34 | } 35 | 36 | public check(): null { 37 | return null; 38 | } 39 | 40 | public build(): null { 41 | return null; 42 | } 43 | 44 | public cache(): void { 45 | // noop 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/guilds/GUILD_DELETE.ts: -------------------------------------------------------------------------------- 1 | import { Action, ClientEvents, extender } from '@klasa/core'; 2 | 3 | import type { GuildDeleteDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: GuildDeleteDispatch): void { 13 | const guild = this.client.guilds.get(data.d.id); 14 | if (!guild) { 15 | if (!data.d.unavailable) return; 16 | 17 | const created = new (extender.get('Guild'))(this.client, data.d, data.shard_id); 18 | if (this.client.options.cache.enabled) { 19 | this.client.guilds.set(created.id, created); 20 | } 21 | 22 | this.client.emit(ClientEvents.GuildUnavailable, created); 23 | return; 24 | } 25 | 26 | const { unavailable } = data.d; 27 | guild.unavailable = unavailable; 28 | 29 | if (unavailable) { 30 | this.client.emit(ClientEvents.GuildUnavailable, guild); 31 | } else { 32 | guild.deleted = true; 33 | this.client.guilds.delete(guild.id); 34 | this.client.emit(ClientEvents.GuildDelete, guild); 35 | } 36 | } 37 | 38 | public check(): null { 39 | return null; 40 | } 41 | 42 | public build(): null { 43 | return null; 44 | } 45 | 46 | public cache(): void { 47 | // noop 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/actions/guilds/GUILD_EMOJIS_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { arrayStrictEquals } from '@klasa/utils'; 2 | import { Action, ClientEvents } from '@klasa/core'; 3 | 4 | import type { GuildEmojisUpdateDispatch } from '@klasa/ws'; 5 | 6 | export default class CoreAction extends Action { 7 | 8 | /** 9 | * Processes the event data from the websocket. 10 | * @since 0.0.1 11 | * @param data The raw data from {@link Client#ws} 12 | */ 13 | public run(data: GuildEmojisUpdateDispatch): void { 14 | const guild = this.client.guilds.get(data.d.guild_id); 15 | if (!guild) return; 16 | 17 | for (const emoji of data.d.emojis) { 18 | const previous = guild.emojis.get(emoji.id as string); 19 | if (!previous) { 20 | // eslint-disable-next-line dot-notation 21 | const built = guild.emojis['_add'](emoji); 22 | this.client.emit(ClientEvents.GuildEmojiCreate, built, guild); 23 | continue; 24 | } 25 | 26 | if (emoji.name !== previous.name || emoji.available !== previous.available || !arrayStrictEquals(emoji.roles ?? [], previous.roleIDs)) { 27 | const clone = previous.clone(); 28 | // eslint-disable-next-line dot-notation 29 | previous['_patch'](emoji); 30 | this.client.emit(ClientEvents.GuildEmojiUpdate, clone, previous, guild); 31 | } 32 | } 33 | 34 | for (const emoji of guild.emojis.values()) { 35 | const exists = data.d.emojis.some(value => value.id === emoji.id); 36 | if (!exists) { 37 | guild.emojis.delete(emoji.id); 38 | this.client.emit(ClientEvents.GuildEmojiDelete, emoji, guild); 39 | } 40 | } 41 | } 42 | 43 | public check(): null { 44 | return null; 45 | } 46 | 47 | public build(): null { 48 | return null; 49 | } 50 | 51 | public cache(): void { 52 | // noop 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/actions/guilds/GUILD_INTEGRATIONS_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, Guild } from '@klasa/core'; 2 | 3 | import type { GuildIntegrationsUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: GuildIntegrationsUpdateDispatch): Guild | null { 8 | return this.client.guilds.get(data.d.guild_id) ?? null; 9 | } 10 | 11 | public build(): null { 12 | return null; 13 | } 14 | 15 | public cache(): void { 16 | // noop 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/actions/guilds/GUILD_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, Guild } from '@klasa/core'; 2 | 3 | import type { GuildCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: GuildCreateDispatch): Guild | null { 8 | return this.client.guilds.get(data.d.id) ?? null; 9 | } 10 | 11 | public build(data: GuildCreateDispatch): Guild { 12 | // eslint-disable-next-line camelcase 13 | return new (extender.get('Guild'))(this.client, data.d, data.shard_id); 14 | } 15 | 16 | public cache(data: Guild): void { 17 | if (this.client.options.cache.enabled) { 18 | this.client.guilds.set(data.id, data); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/actions/guilds/bans/GUILD_BAN_ADD.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, Ban } from '@klasa/core'; 2 | 3 | import type { GuildBanAddDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(): null { 8 | return null; 9 | } 10 | 11 | public build(data: GuildBanAddDispatch): Ban | null { 12 | const guild = this.client.guilds.get(data.d.guild_id); 13 | return guild ? new (extender.get('Ban'))(this.client, data.d, guild) : null; 14 | } 15 | 16 | public cache(data: Ban): void { 17 | if (this.client.options.cache.enabled && data.guild) { 18 | data.guild.bans.set(data.id, data); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/actions/guilds/bans/GUILD_BAN_REMOVE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, Ban } from '@klasa/core'; 2 | 3 | import type { GuildBanAddDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(): null { 8 | return null; 9 | } 10 | 11 | public build(data: GuildBanAddDispatch): Ban | null { 12 | const guild = this.client.guilds.get(data.d.guild_id); 13 | return guild ? new (extender.get('Ban'))(this.client, data.d, guild) : null; 14 | } 15 | 16 | public cache(data: Ban): void { 17 | if (data.guild) data.guild.bans.delete(data.id); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/actions/guilds/members/GUILD_MEMBERS_CHUNK.ts: -------------------------------------------------------------------------------- 1 | import { Action, ClientEvents } from '@klasa/core'; 2 | 3 | import type { GuildMembersChunkDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: GuildMembersChunkDispatch): void { 13 | const guild = this.client.guilds.get(data.d.guild_id); 14 | if (!guild) { 15 | this.client.emit(ClientEvents.Debug, `[GUILD_MEMBERS_CHUNK] Received unknown guild ${data.d.guild_id}.`); 16 | return; 17 | } 18 | 19 | // eslint-disable-next-line dot-notation 20 | const members = data.d.members.map(member => guild.members['_add'](member)); 21 | 22 | for (const presence of data.d.presences ?? []) { 23 | // eslint-disable-next-line dot-notation 24 | this.client.users['_add'](presence.user); 25 | // eslint-disable-next-line dot-notation 26 | guild.presences['_add'](presence); 27 | } 28 | 29 | this.client.emit(this.clientEvent, members, guild, { 30 | chunkCount: data.d.chunk_count, 31 | chunkIndex: data.d.chunk_index, 32 | nonce: data.d.nonce 33 | }); 34 | } 35 | 36 | public check(): null { 37 | return null; 38 | } 39 | 40 | public build(): null { 41 | return null; 42 | } 43 | 44 | public cache(): void { 45 | // noop 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/actions/guilds/members/GUILD_MEMBER_ADD.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, GuildMember } from '@klasa/core'; 2 | 3 | import type { GuildMemberAddDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(): null { 8 | return null; 9 | } 10 | 11 | public build(data: GuildMemberAddDispatch): GuildMember | null { 12 | const guild = this.client.guilds.get(data.d.guild_id); 13 | return guild ? new (extender.get('GuildMember'))(this.client, data.d, guild) : null; 14 | } 15 | 16 | public cache(data: GuildMember): void { 17 | if (data.guild) { 18 | data.guild.members.set(data.id, data); 19 | if (data.guild.memberCount !== null) ++data.guild.memberCount; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/actions/guilds/members/GUILD_MEMBER_REMOVE.ts: -------------------------------------------------------------------------------- 1 | import { Action, GuildMember } from '@klasa/core'; 2 | 3 | import type { GuildMemberRemoveDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: GuildMemberRemoveDispatch): GuildMember | null { 8 | const guild = this.client.guilds.get(data.d.guild_id); 9 | if (!guild) return null; 10 | 11 | if (guild.memberCount !== null) --guild.memberCount; 12 | return guild.members.get(data.d.user.id as string) ?? null; 13 | } 14 | 15 | public build(): GuildMember | null { 16 | return null; 17 | } 18 | 19 | public cache(data: GuildMember): void { 20 | data.deleted = true; 21 | if (data.guild) data.guild.members.delete(data.id); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/guilds/members/GUILD_MEMBER_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, GuildMember } from '@klasa/core'; 2 | 3 | import type { GuildMemberUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: GuildMemberUpdateDispatch): GuildMember | null { 8 | const guild = this.client.guilds.get(data.d.guild_id); 9 | return guild?.members.get(data.d.user.id) ?? null; 10 | } 11 | 12 | public build(data: GuildMemberUpdateDispatch): GuildMember | null { 13 | const guild = this.client.guilds.get(data.d.guild_id); 14 | return guild ? new (extender.get('GuildMember'))(this.client, data.d, guild) : null; 15 | } 16 | 17 | public cache(): void { 18 | // noop 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/actions/guilds/roles/GUILD_ROLE_CREATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, Role } from '@klasa/core'; 2 | 3 | import type { GuildRoleCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(): null { 8 | return null; 9 | } 10 | 11 | public build(data: GuildRoleCreateDispatch): Role | null { 12 | const guild = this.client.guilds.get(data.d.guild_id); 13 | if (!guild) return null; 14 | 15 | return new (extender.get('Role'))(this.client, data.d.role, guild); 16 | } 17 | 18 | public cache(data: Role): void { 19 | if (this.client.options.cache.enabled && data.guild) { 20 | data.guild.roles.set(data.id, data); 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/guilds/roles/GUILD_ROLE_DELETE.ts: -------------------------------------------------------------------------------- 1 | import { Action, Role } from '@klasa/core'; 2 | 3 | import type { GuildRoleDeleteDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: GuildRoleDeleteDispatch): Role | null { 8 | return this.client.guilds.get(data.d.guild_id)?.roles.get(data.d.role_id) ?? null; 9 | } 10 | 11 | public build(): null { 12 | return null; 13 | } 14 | 15 | public cache(data: Role): void { 16 | data.deleted = true; 17 | if (data.guild) data.guild.roles.delete(data.id); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/actions/guilds/roles/GUILD_ROLE_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, Role } from '@klasa/core'; 2 | 3 | import type { GuildRoleCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: GuildRoleCreateDispatch): Role | null { 8 | return this.client.guilds.get(data.d.guild_id)?.roles.get(data.d.role.id) ?? null; 9 | } 10 | 11 | public build(data: GuildRoleCreateDispatch): Role | null { 12 | const guild = this.client.guilds.get(data.d.guild_id); 13 | if (!guild) return null; 14 | 15 | return new (extender.get('Role'))(this.client, data.d.role, guild); 16 | } 17 | 18 | public cache(data: Role): void { 19 | if (this.client.options.cache.enabled && data.guild) { 20 | data.guild.roles.set(data.id, data); 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/messages/MESSAGE_CREATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, extender, Message } from '@klasa/core'; 2 | 3 | import type { MessageCreateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(): null { 8 | return null; 9 | } 10 | 11 | public build(data: MessageCreateDispatch): Message | null { 12 | return new (extender.get('Message'))(this.client, data.d); 13 | } 14 | 15 | public cache(data: Message): void { 16 | if (this.client.options.cache.enabled && data.channel) { 17 | data.author.lastMessageID = data.id; 18 | data.channel.messages.set(data.id, data); 19 | data.channel.lastMessageID = data.id; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/actions/messages/MESSAGE_DELETE.ts: -------------------------------------------------------------------------------- 1 | import { Action, isTextBasedChannel, Message } from '@klasa/core'; 2 | 3 | import type { MessageDeleteDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | public check(data: MessageDeleteDispatch): Message | null { 8 | const guild = data.d.guild_id ? this.client.guilds.get(data.d.guild_id) : undefined; 9 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 10 | if (!channel || !isTextBasedChannel(channel)) return null; 11 | return channel.messages.get(data.d.id) ?? null; 12 | } 13 | 14 | public build(): Message | null { 15 | return null; 16 | } 17 | 18 | public cache(data: Message): void { 19 | data.deleted = true; 20 | if (data.channel) data.channel.messages.delete(data.id); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/actions/messages/MESSAGE_DELETE_BULK.ts: -------------------------------------------------------------------------------- 1 | import { Action, isTextBasedChannel, Message } from '@klasa/core'; 2 | 3 | import type { MessageDeleteBulkDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: MessageDeleteBulkDispatch): void { 13 | const guild = data.d.guild_id ? this.client.guilds.get(data.d.guild_id) : null; 14 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 15 | if (!channel || !isTextBasedChannel(channel)) return; 16 | 17 | const messages: ({ id: string } | Message)[] = []; 18 | for (const id of data.d.ids) { 19 | const message = channel.messages.get(id); 20 | if (message) { 21 | message.deleted = true; 22 | channel.messages.delete(id); 23 | messages.push(message); 24 | } else { 25 | // TODO(kyranet): Maybe make PartialMessage class? 26 | messages.push({ id }); 27 | } 28 | } 29 | 30 | this.client.emit(this.clientEvent, messages, channel); 31 | } 32 | 33 | public check(): null { 34 | return null; 35 | } 36 | 37 | public build(): null { 38 | return null; 39 | } 40 | 41 | public cache(): void { 42 | // noop 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/actions/messages/MESSAGE_REACTION_ADD.ts: -------------------------------------------------------------------------------- 1 | import { Action, isTextBasedChannel, extender, Message, MessageReaction } from '@klasa/core'; 2 | 3 | import type { MessageReactionAddDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: MessageReactionAddDispatch): void { 13 | const guild = data.d.guild_id ? this.client.guilds.get(data.d.guild_id) : undefined; 14 | if (data.d.member && guild) { 15 | // eslint-disable-next-line dot-notation 16 | guild.members['_add'](data.d.member); 17 | } 18 | 19 | const user = this.client.users.get(data.d.user_id); 20 | if (!user) return; 21 | 22 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 23 | if (!channel || !isTextBasedChannel(channel)) return; 24 | 25 | const message = channel.messages.get(data.d.message_id); 26 | if (!message) return; 27 | 28 | const reaction = this.ensureReaction(message, data); 29 | if (user.id === this.client.user?.id) reaction.me = true; 30 | reaction.users.set(user.id); 31 | ++reaction.count; 32 | 33 | this.client.emit(this.clientEvent, reaction, user); 34 | } 35 | 36 | public check(): null { 37 | return null; 38 | } 39 | 40 | public build(): null { 41 | return null; 42 | } 43 | 44 | public cache(): void { 45 | // noop 46 | } 47 | 48 | private ensureReaction(message: Message, data: MessageReactionAddDispatch): MessageReaction { 49 | const reaction = message.reactions.get(data.d.emoji.id || data.d.emoji.name as string); 50 | if (reaction) return reaction; 51 | 52 | const built = new (extender.get('MessageReaction'))(this.client, data.d, message); 53 | message.reactions.set(built.id, built); 54 | return built; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/actions/messages/MESSAGE_REACTION_REMOVE.ts: -------------------------------------------------------------------------------- 1 | import { Action, isTextBasedChannel } from '@klasa/core'; 2 | 3 | import type { MessageReactionRemoveDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: MessageReactionRemoveDispatch): void { 13 | // TODO(VladFrangu): refactor this to remove code dupe from other actions 14 | const guild = (data.d.guild_id && this.client.guilds.get(data.d.guild_id)) ?? null; 15 | const user = this.client.users.get(data.d.user_id); 16 | if (!user) return; 17 | 18 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 19 | if (!channel || !isTextBasedChannel(channel)) return; 20 | 21 | const message = channel.messages.get(data.d.message_id); 22 | if (!message) return; 23 | 24 | const reactionID = data.d.emoji.id ?? data.d.emoji.name as string; 25 | const reaction = message.reactions.get(reactionID); 26 | if (!reaction) return; 27 | 28 | if (reaction.users.delete(data.d.user_id) && --reaction.count === 0) { 29 | message.reactions.delete(reactionID); 30 | } 31 | 32 | if (user.id === this.client.user?.id) { 33 | reaction.me = false; 34 | } 35 | 36 | this.client.emit(this.clientEvent, reaction, user); 37 | } 38 | 39 | public check(): null { 40 | return null; 41 | } 42 | 43 | public build(): null { 44 | return null; 45 | } 46 | 47 | public cache(): void { 48 | // noop 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/actions/messages/MESSAGE_REACTION_REMOVE_ALL.ts: -------------------------------------------------------------------------------- 1 | import { Action, isTextBasedChannel } from '@klasa/core'; 2 | 3 | import type { MessageReactionRemoveAllDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: MessageReactionRemoveAllDispatch): void { 13 | // TODO(VladFrangu): refactor this to remove code dupe from other actions 14 | const guild = (data.d.guild_id && this.client.guilds.get(data.d.guild_id)) ?? null; 15 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 16 | if (!channel || !isTextBasedChannel(channel)) return; 17 | 18 | const message = channel.messages.get(data.d.message_id); 19 | if (!message) return; 20 | 21 | const reactions = message.reactions.clone(); 22 | message.reactions.clear(); 23 | 24 | this.client.emit(this.clientEvent, message, reactions); 25 | } 26 | 27 | public check(): null { 28 | return null; 29 | } 30 | 31 | public build(): null { 32 | return null; 33 | } 34 | 35 | public cache(): void { 36 | // noop 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/actions/messages/MESSAGE_REACTION_REMOVE_EMOJI.ts: -------------------------------------------------------------------------------- 1 | import { Action, isTextBasedChannel } from '@klasa/core'; 2 | 3 | import type { MessageReactionRemoveEmojiDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: MessageReactionRemoveEmojiDispatch): void { 13 | // TODO(VladFrangu): refactor this to remove code dupe from other actions 14 | const guild = (data.d.guild_id && this.client.guilds.get(data.d.guild_id)) ?? null; 15 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 16 | if (!channel || !isTextBasedChannel(channel)) return; 17 | 18 | const message = channel.messages.get(data.d.message_id); 19 | if (!message) return; 20 | 21 | const reactionID = data.d.emoji.id ?? data.d.emoji.name as string; 22 | const reaction = message.reactions.get(reactionID); 23 | if (!reaction) return; 24 | 25 | message.reactions.delete(reaction.id); 26 | this.client.emit(this.clientEvent, reaction); 27 | } 28 | 29 | public check(): null { 30 | return null; 31 | } 32 | 33 | public build(): null { 34 | return null; 35 | } 36 | 37 | public cache(): void { 38 | // noop 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/actions/messages/MESSAGE_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import { Action, isTextBasedChannel } from '@klasa/core'; 2 | 3 | import type { MessageUpdateDispatch } from '@klasa/ws'; 4 | 5 | export default class CoreAction extends Action { 6 | 7 | /** 8 | * Processes the event data from the websocket. 9 | * @since 0.0.1 10 | * @param data The raw data from {@link Client#ws} 11 | */ 12 | public run(data: MessageUpdateDispatch): void { 13 | const guild = data.d.guild_id ? this.client.guilds.get(data.d.guild_id) : undefined; 14 | const channel = guild ? guild.channels.get(data.d.channel_id) : this.client.dms.get(data.d.channel_id); 15 | if (!channel || !isTextBasedChannel(channel)) return; 16 | 17 | const message = channel.messages.get(data.d.id); 18 | if (!message) return; 19 | 20 | const clone = message.clone(); 21 | // eslint-disable-next-line dot-notation 22 | message['_patch'](data.d); 23 | 24 | this.client.emit(this.clientEvent, message, clone); 25 | } 26 | 27 | public check(): null { 28 | return null; 29 | } 30 | 31 | public build(): null { 32 | return null; 33 | } 34 | 35 | public cache(): void { 36 | // noop 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/caching/stores/ChannelPinsStore.ts: -------------------------------------------------------------------------------- 1 | import { RequestOptions, Routes } from '@klasa/rest'; 2 | import { ProxyCache } from '@klasa/cache'; 3 | 4 | import type { APIMessageData } from '@klasa/dapi-types'; 5 | import type { Client } from '../../client/Client'; 6 | import type { Message } from '../structures/messages/Message'; 7 | import type { GuildTextChannel } from '../structures/channels/GuildTextChannel'; 8 | import type { DMChannel } from '../structures/channels/DMChannel'; 9 | 10 | /** 11 | * The store for the pins the channel has. 12 | * @since 0.0.4 13 | */ 14 | export class ChannelPinsStore extends ProxyCache { 15 | 16 | /** 17 | * The {@link Client client} this store belongs to. 18 | * @since 0.0.4 19 | */ 20 | public readonly client: Client; 21 | 22 | /** 23 | * The {@link GuildTextChannel guild channel} or {@link DMChannel DM channel} this store belongs to. 24 | * @since 0.0.4 25 | */ 26 | public readonly channel: GuildTextChannel | DMChannel; 27 | 28 | /** 29 | * Builds the store. 30 | * @since 0.0.4 31 | * @param channel The {@link GuildTextChannel guild channel} or {@link DMChannel DM channel} this store belongs to. 32 | */ 33 | public constructor(channel: GuildTextChannel | DMChannel, keys: string[]) { 34 | super(channel.messages, keys); 35 | this.client = channel.client; 36 | this.channel = channel; 37 | } 38 | 39 | /** 40 | * Pins a message to the channel. 41 | * @since 0.0.4 42 | * @param id The {@link Message#id message id} you want to pin 43 | * @param requestOptions The additional request options. 44 | * @see https://discord.com/developers/docs/resources/channel#add-pinned-channel-message 45 | */ 46 | public async add(id: string, requestOptions: RequestOptions = {}): Promise { 47 | await this.client.api.put(Routes.channelPin(this.channel.id, id), requestOptions); 48 | this.set(id); 49 | return this; 50 | } 51 | 52 | /** 53 | * Removes a pin from the channel given the message ID. 54 | * @since 0.0.4 55 | * @param id The {@link Message#id message id}. 56 | * @param requestOptions The additional request options. 57 | * @see https://discord.com/developers/docs/resources/channel#delete-pinned-channel-message 58 | */ 59 | public async remove(id: string, requestOptions: RequestOptions = {}): Promise { 60 | await this.client.api.delete(Routes.channelPin(this.channel.id, id), requestOptions); 61 | this.delete(id); 62 | return this; 63 | } 64 | 65 | /** 66 | * Returns a list of {@link Message pinned messages}s with their metadata. 67 | * @since 0.0.4 68 | * @see https://discord.com/developers/docs/resources/guild#get-guild-invites 69 | */ 70 | public async fetch(): Promise { 71 | const entries = await this.client.api.get(Routes.channelPins(this.channel.id)) as APIMessageData[]; 72 | for (const entry of entries) { 73 | // eslint-disable-next-line dot-notation 74 | this.channel.messages['_add'](entry); 75 | this.set(entry.id); 76 | } 77 | return this; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/lib/caching/stores/ChannelStore.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RequestOptions } from '@klasa/rest'; 2 | import { Channel } from '../structures/channels/Channel'; 3 | import { DataStore } from './base/DataStore'; 4 | import { extender, Constructor } from '../../util/Extender'; 5 | 6 | import type { APIChannelData } from '@klasa/dapi-types'; 7 | import type { Client } from '../../client/Client'; 8 | import type { GuildChannel } from '../structures/channels/GuildChannel'; 9 | import type { DMChannel } from '../structures/channels/DMChannel'; 10 | import type { Guild } from '../structures/guilds/Guild'; 11 | 12 | /** 13 | * The store for {@link GuildBasedChannel guild based channels}. 14 | * @since 0.0.1 15 | */ 16 | export class ChannelStore extends DataStore { 17 | 18 | /** 19 | * Builds the store. 20 | * @since 0.0.1 21 | * @param client The {@link Client client} this store belongs to. 22 | * @param guild The {@link Guild guild} this store belongs to. 23 | */ 24 | public constructor(client: Client) { 25 | super(client, extender.get('Channel') as Constructor, client.options.cache.limits.channels); 26 | } 27 | 28 | /** 29 | * Removes a channel from the {@link Guild guild}. 30 | * @since 0.0.1 31 | * @param channelID The channel to remove. 32 | * @param requestOptions The additional request options. 33 | * @see https://discord.com/developers/docs/resources/channel#deleteclose-channel 34 | */ 35 | public async remove(channelID: string, requestOptions: RequestOptions = {}): Promise { 36 | const channel = await this.client.api.delete(Routes.channel(channelID), requestOptions) as APIChannelData; 37 | const newChannel = Channel.create(this.client, channel) as Channel; 38 | newChannel.deleted = true; 39 | return newChannel; 40 | } 41 | 42 | /** 43 | * Returns the list of channels as updated from Discord. 44 | * @param id The id for the channel you want to fetch 45 | * @since 0.0.1 46 | * @see https://discord.com/developers/docs/resources/channel#get-channel 47 | */ 48 | public async fetch(id: string): Promise { 49 | const existing = this.get(id); 50 | if (existing) return existing; 51 | const rawChannel = await this.client.api.get(Routes.channel(id)) as APIChannelData; 52 | const channel = this._add(rawChannel); 53 | return channel; 54 | } 55 | 56 | /** 57 | * Adds a new structure to this DataStore 58 | * @param data The data packet to add 59 | */ 60 | protected _add(data: APIChannelData, guild?: Guild): DMChannel | GuildChannel { 61 | let entry: DMChannel | GuildChannel; 62 | // eslint-disable-next-line dot-notation 63 | if (guild) entry = guild.channels['_add'](data); 64 | // eslint-disable-next-line dot-notation, @typescript-eslint/no-non-null-assertion 65 | else if (data.guild_id) entry = this.client.guilds.get(data.guild_id)!.channels['_add'](data); 66 | // eslint-disable-next-line dot-notation 67 | else entry = this.client.dms['_add'](data); 68 | if (this.client.options.cache.enabled) this.set(entry.id, entry); 69 | return entry; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/caching/stores/DMChannelStore.ts: -------------------------------------------------------------------------------- 1 | import { RequestOptions, Routes } from '@klasa/rest'; 2 | import { DataStore } from './base/DataStore'; 3 | import { Channel } from '../structures/channels/Channel'; 4 | import { extender } from '../../util/Extender'; 5 | 6 | import type { APIChannelData } from '@klasa/dapi-types'; 7 | import type { Client } from '../../client/Client'; 8 | import type { DMChannel } from '../structures/channels/DMChannel'; 9 | 10 | /** 11 | * The store for {@link DMChannel DM channels}. 12 | * @since 0.0.1 13 | */ 14 | export class DMChannelStore extends DataStore { 15 | 16 | /** 17 | * Builds the store. 18 | * @since 0.0.1 19 | * @param client The {@link Client client} this store belongs to. 20 | */ 21 | public constructor(client: Client) { 22 | super(client, extender.get('DMChannel'), client.options.cache.limits.dms); 23 | } 24 | 25 | /** 26 | * Closes a channel with a {@link User user}. 27 | * @since 0.0.1 28 | * @param channelID The channel to remove. 29 | * @param requestOptions The additional request options. 30 | * @see https://discord.com/developers/docs/resources/channel#deleteclose-channel 31 | */ 32 | public async remove(channelID: string, requestOptions: RequestOptions = {}): Promise { 33 | const channel = await this.client.api.delete(Routes.channel(channelID), requestOptions) as APIChannelData; 34 | return Channel.create(this.client, channel) as DMChannel; 35 | } 36 | 37 | /** 38 | * Opens a channel with a {@link User user}. 39 | * @since 0.0.1 40 | * @param userID The id for the user to open a dm channel with. 41 | * @see https://discord.com/developers/docs/resources/user#create-dm 42 | */ 43 | public async add(userID: string): Promise { 44 | // eslint-disable-next-line camelcase 45 | const channel = await this.client.api.post(Routes.dms(), { data: { recipient_id: userID } }) as APIChannelData; 46 | // eslint-disable-next-line dot-notation 47 | return this.client.dms['_add'](channel); 48 | } 49 | 50 | /** 51 | * Adds a new structure to this DataStore 52 | * @param data The data packet to add 53 | */ 54 | protected _add(data: APIChannelData): DMChannel { 55 | const existing = this.get(data.id); 56 | // eslint-disable-next-line dot-notation 57 | if (existing && existing.type === data.type) return existing['_patch'](data); 58 | 59 | const entry = Channel.create(this.client, data) as DMChannel; 60 | if (entry && this.client.options.cache.enabled) this.set(entry.id, entry); 61 | return entry; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/lib/caching/stores/GuildChannelInviteStore.ts: -------------------------------------------------------------------------------- 1 | import { RequestOptions, Routes } from '@klasa/rest'; 2 | import { ProxyCache } from '@klasa/cache'; 3 | 4 | import type { APIInviteData, InviteTargetUserType } from '@klasa/dapi-types'; 5 | import type { Client } from '../../client/Client'; 6 | import type { Invite } from '../structures/Invite'; 7 | import type { GuildChannel } from '../structures/channels/GuildChannel'; 8 | 9 | /** 10 | * The store for {@link Invite guild invites} the channel has. 11 | * @since 0.0.3 12 | */ 13 | export class GuildChannelInviteStore extends ProxyCache { 14 | 15 | /** 16 | * The {@link Client client} this store belongs to. 17 | * @since 0.0.3 18 | */ 19 | public readonly client: Client; 20 | 21 | /** 22 | * The {@link GuildChannel guild channel} this store belongs to. 23 | * @since 0.0.3 24 | */ 25 | public readonly channel: GuildChannel; 26 | 27 | /** 28 | * Builds the store. 29 | * @since 0.0.3 30 | * @param channel The {@link GuildChannel guild channel} this store belongs to. 31 | */ 32 | public constructor(channel: GuildChannel, keys: string[]) { 33 | super(channel.client.invites, keys); 34 | this.client = channel.client; 35 | this.channel = channel; 36 | } 37 | 38 | /** 39 | * Creates an invite to the channel. 40 | * @since 0.0.3 41 | * @param data The invite options 42 | * @param requestOptions The additional request options. 43 | * @see https://discord.com/developers/docs/resources/channel#create-channel-invite 44 | */ 45 | public async add(data: GuildChannelInviteStoreAddData, requestOptions: RequestOptions = {}): Promise { 46 | const entry = await this.client.api.post(Routes.channelInvites(this.channel.id), { ...requestOptions, data }) as APIInviteData; 47 | this.set(entry.code); 48 | 49 | // eslint-disable-next-line dot-notation 50 | return this.client.invites['_add'](entry); 51 | } 52 | 53 | /** 54 | * Deletes an invite given its code. 55 | * @since 0.0.3 56 | * @param code The {@link Invite#code invite code}. 57 | * @param requestOptions The additional request options. 58 | * @see https://discord.com/developers/docs/resources/invite#delete-invite 59 | */ 60 | public async remove(code: string, requestOptions: RequestOptions = {}): Promise { 61 | await this.client.api.delete(Routes.invite(code), requestOptions); 62 | return this; 63 | } 64 | 65 | /** 66 | * Returns a list of {@link Invite invite}s with their metadata. 67 | * @since 0.0.3 68 | * @see https://discord.com/developers/docs/resources/guild#get-guild-invites 69 | */ 70 | public async fetch(): Promise { 71 | const entries = await this.client.api.get(Routes.channelInvites(this.channel.id)) as APIInviteData[]; 72 | for (const entry of entries) { 73 | // eslint-disable-next-line dot-notation 74 | this.client.invites['_add'](entry); 75 | this.set(entry.code); 76 | } 77 | return this; 78 | } 79 | 80 | } 81 | 82 | /* eslint-disable camelcase */ 83 | 84 | /** 85 | * The data for {@link GuildChannelInviteStore#add}. 86 | * @since 0.0.3 87 | * @see https://discord.com/developers/docs/resources/channel#create-channel-invite-json-params 88 | */ 89 | export interface GuildChannelInviteStoreAddData { 90 | /** 91 | * Duration of the invite in seconds (0 for it to never expire). 92 | * @since 0.0.3 93 | * @default 86400 (24 hours) 94 | */ 95 | max_age?: number; 96 | 97 | /** 98 | * Max number of uses (0 for unlimited). 99 | * @since 0.0.3 100 | * @default 0 101 | */ 102 | max_uses?: number; 103 | 104 | /** 105 | * Whether this invite only grants temporary membership. 106 | * @since 0.0.3 107 | * @default false 108 | */ 109 | temporary?: boolean; 110 | 111 | /** 112 | * If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). 113 | * @since 0.0.3 114 | * @default false 115 | */ 116 | unique?: boolean; 117 | 118 | /** 119 | * The target user id for this invite. 120 | * @since 0.0.3 121 | */ 122 | target_user?: string; 123 | 124 | /** 125 | * The type of target user for this invite. 126 | * @since 0.0.3 127 | * @see https://discord.com/developers/docs/resources/invite#invite-object-target-user-types 128 | */ 129 | target_user_type?: InviteTargetUserType; 130 | } 131 | -------------------------------------------------------------------------------- /src/lib/caching/stores/GuildEmojiStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { Routes, RequestOptions } from '@klasa/rest'; 3 | import { DataStore } from './base/DataStore'; 4 | import { extender } from '../../util/Extender'; 5 | import { resolveImageToBase64, ImageBufferResolvable } from '../../util/ImageUtil'; 6 | 7 | import type { APIEmojiData } from '@klasa/dapi-types'; 8 | import type { Client } from '../../client/Client'; 9 | import type { GuildEmoji } from '../structures/guilds/GuildEmoji'; 10 | import type { Guild } from '../structures/guilds/Guild'; 11 | 12 | /** 13 | * The store for {@link GuildEmoji guild emojis}. 14 | * @since 0.0.1 15 | */ 16 | export class GuildEmojiStore extends DataStore { 17 | 18 | /** 19 | * The {@link Guild guild} this store belongs to. 20 | * @since 0.0.1 21 | */ 22 | public readonly guild: Guild; 23 | 24 | /** 25 | * Builds the store. 26 | * @since 0.0.1 27 | * @param client The {@link Client client} this store belongs to. 28 | */ 29 | public constructor(client: Client, guild: Guild) { 30 | super(client, extender.get('GuildEmoji'), client.options.cache.limits.emojis); 31 | this.guild = guild; 32 | } 33 | 34 | /** 35 | * Creates a new emoji into the {@link Guild guild} and returns it. 36 | * @since 0.0.1 37 | * @param data The settings for the new emoji. 38 | * @param requestOptions The additional request options. 39 | * @see https://discord.com/developers/docs/resources/emoji#create-guild-emoji 40 | */ 41 | public async add({ image, ...options }: GuildEmojiStoreAddData, requestOptions: RequestOptions = {}): Promise { 42 | const data: GuildEmojiStoreAddData = { 43 | image: await resolveImageToBase64(image), 44 | ...options 45 | }; 46 | const entry = await this.client.api.post(Routes.guildEmojis(this.guild.id), { ...requestOptions, data }) as APIEmojiData; 47 | return this._add(entry); 48 | } 49 | 50 | /** 51 | * Deletes an emoji from the {@link Guild guild}. 52 | * @since 0.0.1 53 | * @param emojiID The {@link GuildEmoji guild emoji} ID. 54 | * @param requestOptions The additional request options. 55 | * @see https://discord.com/developers/docs/resources/emoji#create-guild-emoji 56 | */ 57 | public async remove(emojiID: string, requestOptions: RequestOptions = {}): Promise { 58 | await this.client.api.post(Routes.guildEmoji(this.guild.id, emojiID), requestOptions); 59 | return this; 60 | } 61 | 62 | /** 63 | * Returns all the emojis for the guild. 64 | * @since 0.0.1 65 | * @see https://discord.com/developers/docs/resources/emoji#list-guild-emojis 66 | */ 67 | public fetch(): Promise; 68 | /** 69 | * Returns an emoji given its identifier. 70 | * @since 0.0.1 71 | * @param emoji The {@link GuildEmoji guild emoji} to fetch. 72 | * @see https://discord.com/developers/docs/resources/emoji#get-guild-emoji 73 | */ 74 | public fetch(emoji: string): Promise; 75 | public async fetch(emoji?: string): Promise { 76 | if (emoji) { 77 | const entry = await this.client.api.get(Routes.guildEmoji(this.guild.id, emoji)) as APIEmojiData; 78 | return this._add(entry); 79 | } 80 | 81 | const entries = await this.client.api.get(Routes.guildEmojis(this.guild.id)) as APIEmojiData[]; 82 | for (const entry of entries) this._add(entry); 83 | return this; 84 | } 85 | 86 | /** 87 | * Adds a new structure to this DataStore 88 | * @param data The data packet to add 89 | */ 90 | protected _add(data: APIEmojiData): GuildEmoji { 91 | const existing = this.get(data.id as string); 92 | // eslint-disable-next-line dot-notation 93 | if (existing) return existing['_patch'](data); 94 | 95 | const entry = new this.Holds(this.client, data, this.guild); 96 | if (this.client.options.cache.enabled) this.set(entry.id, entry); 97 | return entry; 98 | } 99 | 100 | } 101 | 102 | /** 103 | * The settings used for {@link GuildEmojiStore#add}. 104 | * @see https://discord.com/developers/docs/resources/emoji#create-guild-emoji-json-params 105 | */ 106 | export interface GuildEmojiStoreAddData { 107 | /** 108 | * The name of the emoji. 109 | */ 110 | name: string; 111 | 112 | /** 113 | * The 128x128 emoji image. 114 | */ 115 | image: ImageBufferResolvable; 116 | 117 | /** 118 | * The roles for which this emoji will be whitelisted. 119 | */ 120 | roles?: readonly string[]; 121 | } 122 | -------------------------------------------------------------------------------- /src/lib/caching/stores/GuildInviteStore.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RequestOptions } from '@klasa/rest'; 2 | import { DataStore } from './base/DataStore'; 3 | import { extender } from '../../util/Extender'; 4 | 5 | import type { APIInviteData } from '@klasa/dapi-types'; 6 | import type { Invite } from '../structures/Invite'; 7 | import type { Guild } from '../structures/guilds/Guild'; 8 | import type { Client } from '../../client/Client'; 9 | 10 | /** 11 | * The store for {@link Invite guild invites}. 12 | * @since 0.0.1 13 | */ 14 | export class GuildInviteStore extends DataStore { 15 | 16 | /** 17 | * The {@link Guild guild} this store belongs to. 18 | * @since 0.0.1 19 | */ 20 | public readonly guild: Guild; 21 | 22 | /** 23 | * Builds the store. 24 | * @since 0.0.1 25 | * @param client The {@link Client client} this store belongs to. 26 | * @param guild The {@link Guild guild} this store belongs to. 27 | */ 28 | public constructor(client: Client, guild: Guild) { 29 | super(client, extender.get('Invite'), client.options.cache.limits.invites); 30 | this.guild = guild; 31 | } 32 | 33 | /** 34 | * Deletes an invite given its code. 35 | * @since 0.0.1 36 | * @param code The {@link Invite#code invite code}. 37 | * @param requestOptions The additional request options. 38 | * @see https://discord.com/developers/docs/resources/invite#delete-invite 39 | */ 40 | public async remove(code: string, requestOptions: RequestOptions = {}): Promise { 41 | const entry = await this.client.api.delete(Routes.invite(code), requestOptions) as APIInviteData; 42 | const channel = this.client.channels.get(entry.channel.id); 43 | return new this.Holds(this.client, entry, channel, this.guild); 44 | } 45 | 46 | /** 47 | * Returns a list of {@link Invite invite}s with their metadata. 48 | * @since 0.0.1 49 | * @see https://discord.com/developers/docs/resources/guild#get-guild-invites 50 | */ 51 | public async fetch(): Promise { 52 | const entries = await this.client.api.get(Routes.guildInvites(this.guild.id)) as APIInviteData[]; 53 | for (const entry of entries) this._add(entry); 54 | return this; 55 | } 56 | 57 | /** 58 | * Adds a new structure to this DataStore 59 | * @param data The data packet to add 60 | * @param cache If the data should be cached 61 | */ 62 | protected _add(data: APIInviteData): Invite { 63 | const existing = this.get(data.code); 64 | // eslint-disable-next-line dot-notation 65 | if (existing) return existing['_patch'](data); 66 | 67 | const channel = this.client.channels.get(data.channel.id); 68 | const entry = new this.Holds(this.client, data, channel, this.guild); 69 | if (this.client.options.cache.enabled) { 70 | this.set(entry.id, entry); 71 | this.client.invites.set(entry.id, entry); 72 | } 73 | return entry; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/caching/stores/GuildMemberRoleStore.ts: -------------------------------------------------------------------------------- 1 | import { ProxyCache } from '@klasa/cache'; 2 | import { Routes, RequestOptions } from '@klasa/rest'; 3 | 4 | import type { GuildMember } from '../structures/guilds/GuildMember'; 5 | import type { Role } from '../structures/guilds/Role'; 6 | import type { Guild } from '../structures/guilds/Guild'; 7 | import type { Client } from '../../client/Client'; 8 | 9 | /** 10 | * The store for {@link Role member roles}. 11 | * @since 0.0.1 12 | */ 13 | export class GuildMemberRoleStore extends ProxyCache { 14 | 15 | /** 16 | * The {@link Client client} this store belongs to. 17 | * @since 0.0.1 18 | */ 19 | public readonly client: Client; 20 | 21 | /** 22 | * The {@link GuildMember guild member} this store belongs to. 23 | * @since 0.0.1 24 | */ 25 | public readonly member: GuildMember; 26 | 27 | /** 28 | * Builds the store. 29 | * @since 0.0.1 30 | * @param member The {@link GuildMember guild member} this store belongs to. 31 | */ 32 | public constructor(member: GuildMember, keys: string[]) { 33 | super(member.guild.roles, keys); 34 | this.client = member.client; 35 | this.member = member; 36 | } 37 | 38 | /** 39 | * Gets the highest role from this store. 40 | * @since 0.0.1 41 | */ 42 | public get highest(): Role | undefined { 43 | return this.reduce((highest, role) => highest.position > role.position ? highest : role, this.firstValue as Role); 44 | } 45 | 46 | /** 47 | * The {@link Guild guild} this store belongs to. 48 | * @since 0.0.1 49 | */ 50 | public get guild(): Guild { 51 | return this.member.guild; 52 | } 53 | 54 | /** 55 | * Adds a {@link Role role} to the {@link GuildMember member}. 56 | * @since 0.0.1 57 | * @param roleID The {@link Role role} ID to add. 58 | * @param requestOptions The additional request options. 59 | * @see https://discord.com/developers/docs/resources/guild#add-guild-member-role 60 | */ 61 | public async add(roleID: string, requestOptions: RequestOptions = {}): Promise { 62 | await this.client.api.put(Routes.guildMemberRole(this.guild.id, this.member.id, roleID), requestOptions); 63 | this.set(roleID); 64 | return this; 65 | } 66 | 67 | /** 68 | * Removes a {@link Role role} from the {@link GuildMember member}. 69 | * @since 0.0.1 70 | * @param roleID The {@link Role role} ID to remove. 71 | * @param requestOptions The additional request options. 72 | * @see https://discord.com/developers/docs/resources/guild#remove-guild-member-role 73 | */ 74 | public async remove(roleID: string, requestOptions: RequestOptions = {}): Promise { 75 | await this.client.api.delete(Routes.guildMemberRole(this.guild.id, this.member.id, roleID), requestOptions); 76 | this.delete(roleID); 77 | return this; 78 | } 79 | 80 | /** 81 | * Modifies all the roles for the {@link GuildMember member}. 82 | * @since 0.0.1 83 | * @param roles A collection of {@link Role role} IDs. 84 | * @param requestOptions The additional request options. 85 | */ 86 | public async modify(roles: readonly string[], requestOptions: RequestOptions = {}): Promise { 87 | await this.member.modify({ roles }, requestOptions); 88 | this.clear(); 89 | for (const role of roles) this.set(role); 90 | return this; 91 | } 92 | 93 | /** 94 | * The JSON representation of this object. 95 | */ 96 | public toJSON(): string[] { 97 | return [...this.keys()]; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/caching/stores/IntegrationStore.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RequestOptions } from '@klasa/rest'; 2 | import { DataStore } from './base/DataStore'; 3 | import { extender } from '../../util/Extender'; 4 | 5 | import type { APIIntegrationData } from '@klasa/dapi-types'; 6 | import type { Client } from '../../client/Client'; 7 | import type { Guild } from '../structures/guilds/Guild'; 8 | import type { Integration } from '../structures/guilds/Integration'; 9 | 10 | /** 11 | * The store for {@link Integration integrations}. 12 | * @since 0.0.1 13 | */ 14 | export class IntegrationStore extends DataStore { 15 | 16 | /** 17 | * The {@link Guild guild} this store belongs to. 18 | * @since 0.0.1 19 | */ 20 | public readonly guild: Guild; 21 | 22 | /** 23 | * Builds the store. 24 | * @since 0.0.1 25 | * @param client The {@link Client client} this store belongs to. 26 | * @param guild The {@link Guild guild} this store belongs to. 27 | */ 28 | public constructor(client: Client, guild: Guild) { 29 | super(client, extender.get('Integration'), client.options.cache.limits.integrations); 30 | this.guild = guild; 31 | } 32 | 33 | /** 34 | * Creates an integration to the {@link Guild guild}. 35 | * @since 0.0.1 36 | * @param data The integration id and type. 37 | * @param requestOptions The additional request options. 38 | * @see https://discord.com/developers/docs/resources/guild#create-guild-integration 39 | */ 40 | public async add(data: IntegrationStoreAddData, requestOptions: RequestOptions = {}): Promise { 41 | await this.client.api.post(Routes.guildIntegrations(this.guild.id), { ...requestOptions, data }); 42 | return this; 43 | } 44 | 45 | /** 46 | * Deletes an integration from the {@link Guild guild}. 47 | * @since 0.0.1 48 | * @param integrationID The {@link Integration integration} ID. 49 | * @param requestOptions The additional request options. 50 | * @see https://discord.com/developers/docs/resources/guild#delete-guild-integration 51 | */ 52 | public async remove(integrationID: string, requestOptions: RequestOptions = {}): Promise { 53 | await this.client.api.delete(Routes.guildIntegration(this.guild.id, integrationID), requestOptions); 54 | return this; 55 | } 56 | 57 | /** 58 | * Returns a collection of {@link Integration integration}s. 59 | * @since 0.0.1 60 | * @see https://discord.com/developers/docs/resources/guild#get-guild-integrations 61 | */ 62 | public async fetch(): Promise { 63 | const entries = await this.client.api.get(Routes.guildIntegrations(this.guild.id)) as APIIntegrationData[]; 64 | for (const entry of entries) this._add(entry); 65 | return this; 66 | } 67 | 68 | /** 69 | * Adds a new structure to this DataStore 70 | * @param data The data packet to add 71 | * @param cache If the data should be cached 72 | */ 73 | protected _add(data: APIIntegrationData): Integration { 74 | const existing = this.get(data.id); 75 | // eslint-disable-next-line dot-notation 76 | if (existing) return existing['_patch'](data); 77 | 78 | const entry = new this.Holds(this.client, data, this.guild); 79 | if (this.client.options.cache.enabled) this.set(entry.id, entry); 80 | return entry; 81 | } 82 | 83 | } 84 | 85 | /** 86 | * The data for {@link IntegrationStore#add}. 87 | * @since 0.0.1 88 | * @see https://discord.com/developers/docs/resources/guild#create-guild-integration-json-params 89 | */ 90 | export interface IntegrationStoreAddData { 91 | /** 92 | * The {@link Integration integration} ID. 93 | * @since 0.0.1 94 | */ 95 | id: string; 96 | 97 | /** 98 | * The {@link Integration integration} type. 99 | * @since 0.0.1 100 | */ 101 | type: string; 102 | } 103 | -------------------------------------------------------------------------------- /src/lib/caching/stores/InviteStore.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RequestOptions } from '@klasa/rest'; 2 | import { DataStore } from './base/DataStore'; 3 | import { extender } from '../../util/Extender'; 4 | 5 | import type { APIInviteData } from '@klasa/dapi-types'; 6 | import type { Invite } from '../structures/Invite'; 7 | import type { Client } from '../../client/Client'; 8 | 9 | /** 10 | * The store for {@link Invite invites}. 11 | * @since 0.0.1 12 | */ 13 | export class InviteStore extends DataStore { 14 | 15 | /** 16 | * Builds the store. 17 | * @since 0.0.1 18 | * @param client The {@link Client client} this store belongs to. 19 | */ 20 | public constructor(client: Client) { 21 | super(client, extender.get('Invite'), client.options.cache.limits.invites); 22 | } 23 | 24 | /** 25 | * Deletes an invite given its code. 26 | * @since 0.0.1 27 | * @param code The {@link Invite#code invite code}. 28 | * @param requestOptions The additional request options. 29 | * @see https://discord.com/developers/docs/resources/invite#delete-invite 30 | */ 31 | public async remove(code: string, requestOptions: RequestOptions = {}): Promise { 32 | const entry = await this.client.api.delete(Routes.invite(code), requestOptions) as APIInviteData; 33 | const guild = entry.guild ? this.client.guilds.get(entry.guild.id) : null; 34 | const channel = this.client.channels.get(entry.channel.id); 35 | return new this.Holds(this.client, entry, channel, guild); 36 | } 37 | 38 | /** 39 | * Returns a {@link Invite invite} with optionally their metadata. 40 | * @since 0.0.1 41 | * @param code The {@link Invite#code invite code}. 42 | * @see https://discord.com/developers/docs/resources/invite#get-invite 43 | */ 44 | public async fetch(code: string, options: InviteStoreFetchOptions = {}): Promise { 45 | const entry = await this.client.api.get(Routes.invite(code), { query: Object.entries(options) }) as APIInviteData; 46 | return this._add(entry); 47 | } 48 | 49 | /** 50 | * Adds a new structure to this DataStore 51 | * @param data The data packet to add 52 | * @param cache If the data should be cached 53 | */ 54 | protected _add(data: APIInviteData): Invite { 55 | const existing = this.get(data.code); 56 | // eslint-disable-next-line dot-notation 57 | if (existing) return existing['_patch'](data); 58 | 59 | const guild = data.guild ? this.client.guilds.get(data.guild.id) : null; 60 | const channel = this.client.channels.get(data.channel.id); 61 | const entry = new this.Holds(this.client, data, channel, guild); 62 | if (this.client.options.cache.enabled) this.set(entry.id, entry); 63 | return entry; 64 | } 65 | 66 | } 67 | 68 | /* eslint-disable camelcase */ 69 | 70 | /** 71 | * The options for {@link InviteStore#fetch}. 72 | * @since 0.0.1 73 | * @see https://discord.com/developers/docs/resources/invite#get-invite-get-invite-url-parameters 74 | */ 75 | export interface InviteStoreFetchOptions { 76 | /** 77 | * Whether the invite should contain approximate member counts. 78 | * @since 0.0.1 79 | * @default false 80 | */ 81 | with_counts?: boolean; 82 | } 83 | 84 | /* eslint-enable camelcase */ 85 | -------------------------------------------------------------------------------- /src/lib/caching/stores/MessageReactionStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { Routes } from '@klasa/rest'; 3 | import { DataStore } from './base/DataStore'; 4 | import { ReactionIterator, ReactionIteratorOptions } from '../../util/iterators/ReactionIterator'; 5 | import { extender } from '../../util/Extender'; 6 | import { EmojiResolvable, resolveEmoji } from '../../util/Util'; 7 | 8 | import type { Client } from '../../client/Client'; 9 | import type { MessageReaction } from '../structures/messages/reactions/MessageReaction'; 10 | import type { Message } from '../structures/messages/Message'; 11 | import type { User } from '../structures/User'; 12 | 13 | /** 14 | * The store for {@link MessageReaction message reactions}. 15 | * @since 0.0.1 16 | */ 17 | export class MessageReactionStore extends DataStore { 18 | 19 | /** 20 | * The {@link Message message} this store belongs to. 21 | * @since 0.0.1 22 | */ 23 | public readonly message: Message; 24 | 25 | /** 26 | * Builds the store. 27 | * @since 0.0.1 28 | * @param client The {@link Client client} this store belongs to. 29 | * @param message The {@link Message message} this store belongs to. 30 | */ 31 | public constructor(client: Client, message: Message) { 32 | super(client, extender.get('MessageReaction'), client.options.cache.limits.reactions); 33 | this.message = message; 34 | } 35 | 36 | /** 37 | * Adds a reaction to the message. 38 | * @param emoji The emoji to be added as a reaction to this message. 39 | * @since 0.0.1 40 | * @see https://discord.com/developers/docs/resources/channel#create-reaction 41 | */ 42 | public async add(emoji: EmojiResolvable): Promise { 43 | await this.client.api.put(Routes.messageReactionUser(this.message.channel.id, this.message.id, resolveEmoji(emoji), '@me')); 44 | return this; 45 | } 46 | 47 | /** 48 | * Deletes all reactions on a message. 49 | * @since 0.0.1 50 | * @see https://discord.com/developers/docs/resources/channel#delete-all-reactions 51 | */ 52 | public remove(): Promise; 53 | /** 54 | * Deletes a reaction from a message. 55 | * @since 0.0.1 56 | * @param emoji The emoji to remove from the message's reactions. 57 | * @see https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji 58 | */ 59 | public remove(emoji: EmojiResolvable): Promise; 60 | public async remove(emoji?: EmojiResolvable): Promise { 61 | await this.client.api.delete(emoji ? 62 | Routes.messageReaction(this.message.channel.id, this.message.id, resolveEmoji(emoji)) : 63 | Routes.messageReactions(this.message.channel.id, this.message.id)); 64 | return this; 65 | } 66 | 67 | /** 68 | * Asynchronously iterator over received reactions. 69 | * @since 0.0.1 70 | * @param options Any options to pass to the iterator. 71 | */ 72 | public async *iterate(options?: ReactionIteratorOptions): AsyncIterableIterator<[MessageReaction, User]> { 73 | yield* new ReactionIterator(this.message, options); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/caching/stores/MessageReactionUserStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { ProxyCache } from '@klasa/cache'; 3 | import { Routes } from '@klasa/rest'; 4 | 5 | import type { APIUserData } from '@klasa/dapi-types'; 6 | import type { MessageReaction } from '../structures/messages/reactions/MessageReaction'; 7 | import type { Message } from '../structures/messages/Message'; 8 | import type { User } from '../structures/User'; 9 | import type { Client } from '../../client/Client'; 10 | 11 | /** 12 | * The store for {@link MessageReaction message reaction} {@link User users}. 13 | * @since 0.0.1 14 | */ 15 | export class MessageReactionUserStore extends ProxyCache { 16 | 17 | /** 18 | * The {@link Client client} this store belongs to. 19 | * @since 0.0.1 20 | */ 21 | public readonly client: Client; 22 | 23 | /** 24 | * The {@link MessageReaction message reaction} this store belongs to. 25 | * @since 0.0.1 26 | */ 27 | public readonly reaction: MessageReaction; 28 | 29 | /** 30 | * Builds the store. 31 | * @since 0.0.1 32 | * @param reaction The {@link MessageReaction message reaction} this store belongs to. 33 | */ 34 | public constructor(reaction: MessageReaction) { 35 | super(reaction.client.users, []); 36 | this.client = reaction.client; 37 | this.reaction = reaction; 38 | } 39 | 40 | /** 41 | * The {@link Message message} this store belongs to. 42 | * @since 0.0.1 43 | */ 44 | public get message(): Message { 45 | return this.reaction.message; 46 | } 47 | 48 | /** 49 | * Adds a reaction to the message. 50 | * @since 0.0.1 51 | * @see https://discord.com/developers/docs/resources/channel#create-reaction 52 | */ 53 | public async add(): Promise { 54 | await this.message.reactions.add(this.reaction.emoji); 55 | return this; 56 | } 57 | 58 | /** 59 | * Removes a reaction from the {@link Client#user client user}. 60 | * @since 0.0.1 61 | * @param userID The bot {@link User user}'s ID or `@me`. 62 | * @see https://discord.com/developers/docs/resources/channel#delete-own-reaction 63 | */ 64 | public remove(userID?: '@me'): Promise; 65 | /** 66 | * Remove a reaction from a user. 67 | * @since 0.0.1 68 | * @param userID The {@link User user}'s ID. 69 | * @see https://discord.com/developers/docs/resources/channel#delete-user-reaction 70 | */ 71 | public remove(userID: string): Promise; 72 | public async remove(userID = '@me'): Promise { 73 | await this.client.api.delete(Routes.messageReactionUser(this.message.channel.id, this.message.id, this.reaction.emoji.identifier, userID === this.client.user?.id ? '@me' : userID)); 74 | return this; 75 | } 76 | 77 | /** 78 | * Fetches all the users, populating {@link MessageReactionEmoji#users}. 79 | * @since 0.0.1 80 | * @param options The options for the fetch 81 | */ 82 | public async fetch(options?: MessageReactionFetchOptions): Promise { 83 | const users = await this.client.api.get(Routes.messageReaction(this.message.channel.id, this.message.id, this.reaction.emoji.identifier), { 84 | query: options && Object.entries(options) 85 | }) as APIUserData[]; 86 | for (const user of users) { 87 | // eslint-disable-next-line dot-notation 88 | this.client.users['_add'](user); 89 | this.set(user.id); 90 | } 91 | 92 | return this; 93 | } 94 | 95 | } 96 | 97 | /** 98 | * @see https://discord.com/developers/docs/resources/channel#get-reactions-query-string-params 99 | */ 100 | export interface MessageReactionFetchOptions { 101 | /** 102 | * Get users before this user ID. 103 | * @since 0.0.1 104 | */ 105 | before?: string; 106 | 107 | /** 108 | * Get users after this user ID. 109 | * @since 0.0.1 110 | */ 111 | after?: string; 112 | 113 | /** 114 | * Max number of users to return (1-100). 115 | * @since 0.0.1 116 | */ 117 | limit?: number; 118 | } 119 | -------------------------------------------------------------------------------- /src/lib/caching/stores/OverwriteStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { RequestOptions, Routes } from '@klasa/rest'; 3 | import { DataStore } from './base/DataStore'; 4 | import { extender } from '../../util/Extender'; 5 | import { GuildMember } from '../structures/guilds/GuildMember'; 6 | 7 | import type { APIOverwriteData } from '@klasa/dapi-types'; 8 | import type { Client } from '../../client/Client'; 9 | import type { Role } from '../structures/guilds/Role'; 10 | import type { Overwrite, OverwriteData } from '../structures/guilds/Overwrite'; 11 | import type { GuildChannel } from '../structures/channels/GuildChannel'; 12 | 13 | export interface MemberOverwrites { 14 | everyone?: Overwrite; 15 | roles: Overwrite[]; 16 | member?: Overwrite; 17 | } 18 | 19 | export interface RoleOverwrites { 20 | everyone?: Overwrite; 21 | role?: Overwrite; 22 | } 23 | 24 | /** 25 | * The store for {@link Overwrite Overwrites}. 26 | * @since 0.0.1 27 | */ 28 | export class OverwriteStore extends DataStore { 29 | 30 | /** 31 | * The {@link GuildChannel channel} this store belongs to. 32 | * @since 0.0.1 33 | */ 34 | public readonly channel: GuildChannel; 35 | 36 | /** 37 | * Builds the store. 38 | * @since 0.0.1 39 | * @param client The {@link Client client} this store belongs to. 40 | * @param client The {@link Client client} this store belongs to. 41 | */ 42 | public constructor(client: Client, channel: GuildChannel) { 43 | super(client, extender.get('Overwrite'), client.options.cache.limits.overwrites); 44 | this.channel = channel; 45 | } 46 | 47 | /** 48 | * Creates a new {@link Overwrite overwrite} for the {@link GuildChannel channel}. 49 | * @since 0.0.1 50 | * @param id The id the overwrite is for 51 | * @param data The overwrite data. 52 | * @param requestOptions The additional request options. 53 | * @see https://discord.com/developers/docs/resources/channel#edit-channel-permissions 54 | */ 55 | public async add(id: string, data: OverwriteData, requestOptions: RequestOptions = {}): Promise { 56 | await this.client.api.put(Routes.channelPermissions(this.channel.id, id), { ...requestOptions, data }); 57 | return this._add({ id, ...data }); 58 | } 59 | 60 | /** 61 | * Deletes a {@link Overwrite overwrite} from the {@link GuildChannel channel}. 62 | * @since 0.0.1 63 | * @param overwriteID The {@link Role role} ID to delete. 64 | * @param requestOptions The additional request options. 65 | * @see https://discord.com/developers/docs/resources/channel#delete-channel-permission 66 | */ 67 | public async remove(overwriteID: string, requestOptions: RequestOptions = {}): Promise { 68 | await this.client.api.delete(Routes.channelPermissions(this.channel.id, overwriteID), requestOptions); 69 | return this; 70 | } 71 | 72 | /** 73 | * Gets the overwrites for a given guild member or role. 74 | * @param target 75 | */ 76 | public for(target: GuildMember): MemberOverwrites 77 | public for(target: Role): RoleOverwrites 78 | public for(target: GuildMember | Role): MemberOverwrites | RoleOverwrites { 79 | const everyone = this.get(this.channel.guild.id); 80 | 81 | if (target instanceof GuildMember) { 82 | const member = this.get(target.id); 83 | const roles: Overwrite[] = []; 84 | 85 | for (const overwrite of this.values()) { 86 | if (target.roles.has(overwrite.id)) roles.push(overwrite); 87 | } 88 | 89 | return { everyone, roles, member }; 90 | } 91 | 92 | const role = this.get(target.id); 93 | 94 | return { everyone, role }; 95 | } 96 | 97 | /** 98 | * Adds a new structure to this DataStore 99 | * @param data The data packet to add 100 | */ 101 | protected _add(data: APIOverwriteData): Overwrite { 102 | const existing = this.get(data.id); 103 | // eslint-disable-next-line dot-notation 104 | if (existing) return existing['_patch'](data); 105 | 106 | const entry = new this.Holds(this.client, data, this.channel); 107 | if (this.client.options.cache.enabled) this.set(entry.id, entry); 108 | return entry; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/lib/caching/stores/PresenceStore.ts: -------------------------------------------------------------------------------- 1 | import { DataStore } from './base/DataStore'; 2 | import { extender } from '../../util/Extender'; 3 | 4 | import type { APIPresenceUpdateData } from '@klasa/dapi-types'; 5 | import type { Presence } from '../structures/guilds/Presence'; 6 | import type { Client } from '../../client/Client'; 7 | import type { Guild } from '../structures/guilds/Guild'; 8 | 9 | /** 10 | * The store for {@link Presence presences}. 11 | * @since 0.0.1 12 | */ 13 | export class PresenceStore extends DataStore { 14 | 15 | /** 16 | * The {@link Guild guild} this store belongs to. 17 | * @since 0.0.1 18 | */ 19 | public readonly guild: Guild; 20 | 21 | /** 22 | * Builds the store. 23 | * @since 0.0.1 24 | * @param client The {@link Client client} this store belongs to. 25 | * @param guild The {@link Guild guild} this store belongs to. 26 | */ 27 | public constructor(client: Client, guild: Guild) { 28 | super(client, extender.get('Presence'), client.options.cache.limits.presences); 29 | this.guild = guild; 30 | } 31 | 32 | /** 33 | * Adds a new structure to this DataStore 34 | * @param data The data packet to add 35 | */ 36 | protected _add(data: APIPresenceUpdateData): Presence { 37 | const existing = this.get(data.user.id); 38 | // eslint-disable-next-line dot-notation 39 | if (existing) return existing['_patch'](data); 40 | 41 | const entry = new this.Holds(this.client, data); 42 | if (this.client.options.cache.enabled) this.set(entry.id, entry); 43 | return entry; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/caching/stores/UserStore.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { Routes } from '@klasa/rest'; 3 | import { DataStore } from './base/DataStore'; 4 | import { extender } from '../../util/Extender'; 5 | import { GuildMember } from '../structures/guilds/GuildMember'; 6 | import { Message } from '../structures/messages/Message'; 7 | 8 | import type { APIUserData } from '@klasa/dapi-types'; 9 | import type { User } from '../structures/User'; 10 | import type { Client } from '../../client/Client'; 11 | 12 | /** 13 | * The store for {@link User users}. 14 | * @since 0.0.1 15 | */ 16 | export class UserStore extends DataStore { 17 | 18 | /** 19 | * Builds the store. 20 | * @since 0.0.1 21 | * @param client The {@link Client client} this store belongs to. 22 | */ 23 | public constructor(client: Client) { 24 | super(client, extender.get('User'), client.options.cache.limits.users); 25 | } 26 | 27 | /** 28 | * Gets a {@link User user} by its ID. 29 | * @since 0.0.1 30 | * @param userID The {@link User user} ID. 31 | * @see https://discord.com/developers/docs/resources/user#get-user 32 | */ 33 | public async fetch(userID: string): Promise { 34 | const data = await this.client.api.get(Routes.user(userID)) as APIUserData; 35 | return this._add(data); 36 | } 37 | 38 | /** 39 | * Resolves data into Structures 40 | * @param data The data to resolve 41 | */ 42 | public resolve(data: unknown): User | null { 43 | if (data instanceof GuildMember) return data.user; 44 | if (data instanceof Message) return data.author; 45 | return super.resolve(data); 46 | } 47 | 48 | /** 49 | * Resolves data into ids 50 | * @param data The data to resolve 51 | */ 52 | public resolveID(data: unknown): string | null { 53 | if (data instanceof GuildMember) return data.user && data.user.id; 54 | if (data instanceof Message) return data.author.id; 55 | return super.resolveID(data); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/caching/stores/VoiceStateStore.ts: -------------------------------------------------------------------------------- 1 | import { DataStore } from './base/DataStore'; 2 | import { extender } from '../../util/Extender'; 3 | 4 | import type { APIVoiceStatePartial } from '@klasa/dapi-types'; 5 | import type { Client } from '../../client/Client'; 6 | import type { VoiceState } from '../structures/guilds/VoiceState'; 7 | import type { Guild } from '../structures/guilds/Guild'; 8 | 9 | /** 10 | * The store for {@link VoiceState voice states}. 11 | * @since 0.0.1 12 | */ 13 | export class VoiceStateStore extends DataStore { 14 | 15 | /** 16 | * The {@link Guild guild} this store belongs to. 17 | * @since 0.0.1 18 | */ 19 | public readonly guild: Guild; 20 | 21 | /** 22 | * Builds the store. 23 | * @since 0.0.1 24 | * @param client The {@link Client client} this store belongs to. 25 | * @param guild The {@link Guild guild} this store belongs to. 26 | */ 27 | public constructor(client: Client, guild: Guild) { 28 | super(client, extender.get('VoiceState'), client.options.cache.limits.voiceStates); 29 | this.guild = guild; 30 | } 31 | 32 | /** 33 | * Adds a new structure to this DataStore 34 | * @param data The data packet to add 35 | */ 36 | protected _add(data: APIVoiceStatePartial): VoiceState { 37 | const existing = this.get(data.user_id); 38 | // eslint-disable-next-line dot-notation 39 | if (existing) return existing['_patch'](data); 40 | 41 | const entry = new this.Holds(this.client, data, this.guild); 42 | if (this.client.options.cache.enabled) this.set(entry.id, entry); 43 | return entry; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/caching/stores/base/DataStore.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@klasa/cache'; 2 | 3 | import type { Client } from '../../../client/Client'; 4 | import type { Structure } from '../../structures/base/Structure'; 5 | import type { Constructor } from '../../../util/Extender'; 6 | 7 | /** 8 | * The data caches with extra methods unique to each data store 9 | */ 10 | export class DataStore extends Cache { 11 | 12 | /** 13 | * The cache limit of this DataStore 14 | */ 15 | #limit: number; 16 | 17 | public constructor(public readonly client: Client, protected readonly Holds: Constructor, limit: number, iterable?: Iterable) { 18 | super(); 19 | this.#limit = limit; 20 | if (iterable) for (const item of iterable) this._add(item); 21 | } 22 | 23 | /** 24 | * Resolves data into Structures 25 | * @param data The data to resolve 26 | */ 27 | public resolve(data: unknown): S | null { 28 | if (typeof data === 'string') return this.get(data) || null; 29 | if (data instanceof this.Holds) return data; 30 | return null; 31 | } 32 | 33 | /** 34 | * Resolves data into ids 35 | * @param data The data to resolve 36 | */ 37 | public resolveID(data: unknown): string | null { 38 | if (typeof data === 'string') return data; 39 | if (data instanceof this.Holds) return data.id; 40 | return null; 41 | } 42 | 43 | /** 44 | * Sets a value to this DataStore taking into account the cache limit. 45 | * @param key The key of the value you are setting 46 | * @param value The value for the key you are setting 47 | */ 48 | public set(key: string, value: S): this { 49 | if (this.#limit === 0) return this; 50 | if (this.size >= this.#limit && !this.has(key)) this.delete(this.firstKey as string); 51 | return super.set(key, value); 52 | } 53 | 54 | /** 55 | * Adds a new structure to this DataStore 56 | * @param data The data packet to add 57 | * @param cache If the data should be cached 58 | */ 59 | protected _add(data: Record): S { 60 | const existing = this.get(data.id as string); 61 | // eslint-disable-next-line dot-notation 62 | if (existing) return existing['_patch'](data); 63 | 64 | const entry = new this.Holds(this.client, data); 65 | if (this.client.options.cache.enabled) this.set(entry.id, entry); 66 | return entry; 67 | } 68 | 69 | /** 70 | * Defines the extensibility of DataStores 71 | */ 72 | public static get [Symbol.species](): typeof Cache { 73 | return Cache; 74 | } 75 | 76 | /** 77 | * The JSON representation of this object. 78 | */ 79 | public toJSON(): string[] { 80 | return [...this.keys()]; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/caching/structures/Attachment.ts: -------------------------------------------------------------------------------- 1 | import { basename } from 'path'; 2 | import { Readable } from 'stream'; 3 | import { promises as fsp } from 'fs'; 4 | import fetch from 'node-fetch'; 5 | import { pathExists } from 'fs-nextra'; 6 | import { MessageAttachment } from './messages/MessageAttachment'; 7 | 8 | import type { File } from '@klasa/rest'; 9 | 10 | export class Attachment { 11 | 12 | /** 13 | * The name of the Attachment 14 | */ 15 | public name?: string; 16 | 17 | /** 18 | * The unresolved file to send to the api 19 | */ 20 | public file?: string | Readable | Buffer | MessageAttachment; 21 | 22 | public constructor(attachment: Partial = {}) { 23 | this.name = attachment.name; 24 | this.file = attachment.file; 25 | } 26 | 27 | /** 28 | * Allows you to set the name of the attachment 29 | * @param name The name of the Attachment 30 | */ 31 | public setName(name: string): this { 32 | this.name = name; 33 | return this; 34 | } 35 | 36 | /** 37 | * Allows you to set the file of the attachment 38 | * @param file The unresolved file to send to the api 39 | */ 40 | public setFile(file: string | Readable | Buffer | MessageAttachment): this { 41 | this.file = file; 42 | return this; 43 | } 44 | 45 | /** 46 | * Resolves a stream, url, file location, or text into a buffer we can send to the api 47 | */ 48 | public async resolve(): Promise { 49 | if (!this.file) throw new Error('Cannot resolve a FileAttachment that doesn\'t include a file'); 50 | 51 | if (this.file instanceof Readable) { 52 | this.name = this.name || 'file.dat'; 53 | const buffers = []; 54 | for await (const buffer of this.file) buffers.push(buffer); 55 | this.file = Buffer.concat(buffers); 56 | } else if (Buffer.isBuffer(this.file)) { 57 | this.name = this.name || 'file.txt'; 58 | } else if (this.file instanceof MessageAttachment) { 59 | this.name = this.file.filename; 60 | this.file = await (await fetch(this.file.url)).buffer(); 61 | } else if (/^https?:\/\//.test(this.file)) { 62 | this.name = this.name || basename(this.file); 63 | this.file = await (await fetch(this.file)).buffer(); 64 | } else if (await pathExists(this.file)) { 65 | this.name = this.name || basename(this.file); 66 | this.file = await fsp.readFile(this.file); 67 | } else { 68 | this.name = this.name || 'file.txt'; 69 | this.file = Buffer.from(this.file); 70 | } 71 | 72 | return this as File; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/caching/structures/ClientUser.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@klasa/rest'; 2 | import { User } from './User'; 3 | import { ClientPresence } from './presences/ClientPresence'; 4 | import { ImageBufferResolvable, resolveImageToBase64 } from '../../util/ImageUtil'; 5 | 6 | import type { APIUserData } from '@klasa/dapi-types'; 7 | import type { Client } from '../../client/Client'; 8 | 9 | /** 10 | * Represents the client's user account. 11 | * @see https://discord.com/developers/docs/resources/user#user-object 12 | */ 13 | export class ClientUser extends User { 14 | 15 | /** 16 | * The client presence. 17 | * @since 0.0.1 18 | */ 19 | public presence: ClientPresence; 20 | 21 | public constructor(client: Client, data: APIUserData) { 22 | super(client, data); 23 | this.presence = new ClientPresence(this.client, { user: { id: this.id } }); 24 | } 25 | 26 | /** 27 | * Modifies the client user. 28 | * @since 0.0.1 29 | * @param options The options to be set. 30 | * @see https://discord.com/developers/docs/resources/user#modify-current-user 31 | */ 32 | public async modify({ avatar, ...options }: ClientUserModifyOptions): Promise { 33 | const data: ClientUserModifyOptions = { 34 | avatar: avatar ? await resolveImageToBase64(avatar) : avatar, 35 | ...options 36 | }; 37 | const entry = await this.client.api.patch(Routes.user(), { data }) as APIUserData; 38 | return this._patch(entry); 39 | } 40 | 41 | /** 42 | * Modifies the client user's username. 43 | * @since 0.0.1 44 | * @param username The username to be set. 45 | * @see https://discord.com/developers/docs/resources/user#modify-current-user 46 | */ 47 | public setUsername(username: string): Promise { 48 | return this.modify({ username }); 49 | } 50 | 51 | /** 52 | * Modifies the client user's avatar. 53 | * @since 0.0.1 54 | * @param avatar The avatar to be set. 55 | * @see https://discord.com/developers/docs/resources/user#modify-current-user 56 | */ 57 | public setAvatar(avatar: ImageBufferResolvable): Promise { 58 | return this.modify({ avatar }); 59 | } 60 | 61 | } 62 | 63 | /** 64 | * The options for {@link ClientUser#modify}. 65 | * @since 0.0.1 66 | * @see https://discord.com/developers/docs/resources/user#modify-current-user-json-params 67 | */ 68 | export interface ClientUserModifyOptions { 69 | /** 70 | * User's username, if changed may cause the user's discriminator to be randomized. 71 | * @since 0.0.1 72 | */ 73 | username?: string; 74 | 75 | /** 76 | * If passed, modifies the user's avatar 77 | * @since 0.0.1 78 | */ 79 | avatar?: ImageBufferResolvable | null; 80 | } 81 | -------------------------------------------------------------------------------- /src/lib/caching/structures/Invite.ts: -------------------------------------------------------------------------------- 1 | import { Structure } from './base/Structure'; 2 | 3 | import type { APIInviteData, InviteTargetUserType } from '@klasa/dapi-types'; 4 | import type { Client } from '../../client/Client'; 5 | import type { Guild } from './guilds/Guild'; 6 | import type { Channel } from './channels/Channel'; 7 | import type { User } from './User'; 8 | 9 | /** 10 | * @see https://discord.com/developers/docs/resources/invite#invite-object 11 | */ 12 | export class Invite extends Structure { 13 | 14 | /** 15 | * The invite code. 16 | * @since 0.0.1 17 | */ 18 | public readonly id: string; 19 | 20 | /** 21 | * The guild this invite is for. 22 | * @since 0.0.1 23 | */ 24 | public readonly guild: Guild | null; 25 | 26 | /** 27 | * The channel this invite is for. 28 | * @since 0.0.1 29 | */ 30 | public readonly channel: Channel; 31 | 32 | /** 33 | * The user who created the invite. 34 | * @since 0.0.1 35 | */ 36 | public inviter!: User | null; 37 | 38 | /** 39 | * The target user for this invite. 40 | * @since 0.0.1 41 | */ 42 | public targetUser!: User | null; 43 | 44 | /** 45 | * The type of user target for this invite. 46 | * @since 0.0.1 47 | * @see https://discord.com/developers/docs/resources/invite#invite-object-target-user-types 48 | */ 49 | public targetUserType!: InviteTargetUserType | null; 50 | 51 | /** 52 | * Approximate count of online members (only present when `target_user` is set). 53 | * @since 0.0.1 54 | */ 55 | public approximatePresenceCount!: number | null; 56 | 57 | /** 58 | * Approximate count of total members. 59 | * @since 0.0.1 60 | */ 61 | public approximateMemberCount!: number | null; 62 | 63 | public constructor(client: Client, data: APIInviteData, channel: Channel, guild?: Guild) { 64 | super(client); 65 | 66 | this.id = data.code; 67 | this.channel = channel; 68 | this.guild = guild ?? null; 69 | this._patch(data); 70 | } 71 | 72 | protected _patch(data: APIInviteData): this { 73 | this.inviter = (data.inviter && this.client.users.get(data.inviter.id)) ?? null; 74 | this.targetUser = (data.target_user && this.client.users.get(data.target_user.id)) ?? null; 75 | this.targetUserType = data.target_user_type ?? null; 76 | this.approximatePresenceCount = data.approximate_presence_count ?? null; 77 | this.approximateMemberCount = data.approximate_member_count ?? null; 78 | return this; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/caching/structures/Typing.ts: -------------------------------------------------------------------------------- 1 | import { TimerManager } from '@klasa/timer-manager'; 2 | import { Routes } from '@klasa/rest'; 3 | 4 | import type { DMChannel } from './channels/DMChannel'; 5 | import type { Client } from '../../client/Client'; 6 | import type { GuildTextChannel } from './channels/GuildTextChannel'; 7 | 8 | /** 9 | * Handles typing indication sending in text channels 10 | */ 11 | export class Typing { 12 | 13 | /** 14 | * The client this typing manager is for. 15 | * @since 0.0.1 16 | */ 17 | public readonly client: Client; 18 | 19 | /** 20 | * The channel this typing manager is for. 21 | */ 22 | public readonly channel: GuildTextChannel | DMChannel; 23 | 24 | /** 25 | * The internal typing counter (allows handling of multiple commands in the same channel). 26 | * @since 0.0.1 27 | */ 28 | #count = 0; 29 | 30 | /** 31 | * The internal interval to fire typing indications 32 | * @since 0.0.1 33 | */ 34 | #interval: NodeJS.Timeout | null = null; 35 | 36 | public constructor(channel: GuildTextChannel | DMChannel) { 37 | this.client = channel.client; 38 | this.channel = channel; 39 | } 40 | 41 | /** 42 | * Ups the internal typing counter and starts typing if not already. 43 | * @param count How much to increase the internal counter. (Typically leave this at the default 1) 44 | * @since 0.0.1 45 | */ 46 | public start(count = 1): void { 47 | this.#count += count; 48 | if (!this.#interval) this._startTyping(); 49 | } 50 | 51 | /** 52 | * Lowers the internal typing counter and stops typing if the counter reaches 0 (or less). 53 | * @param count How much to decrease the internal counter. (Typically leave this at the default 1) 54 | * @since 0.0.1 55 | */ 56 | public stop(count = 1): void { 57 | this.#count -= count; 58 | if (this.#count < 0) this.#count = 0; 59 | if (!this.#count) this._stopTyping(); 60 | } 61 | 62 | /** 63 | * An alias for Typing#stop(Infinity). Forces the counter back to 0, and stops typing. 64 | * @since 0.0.1 65 | */ 66 | public forceStop(): void { 67 | return this.stop(Infinity); 68 | } 69 | 70 | /** 71 | * Internal method to start the typing interval if not already started. 72 | * @since 0.0.1 73 | */ 74 | protected _startTyping(): void { 75 | if (!this.#interval) { 76 | this._type(); 77 | this.#interval = TimerManager.setInterval(this._type.bind(this), 9000); 78 | } 79 | } 80 | 81 | /** 82 | * Internal method to send a typing indicator. 83 | * @since 0.0.1 84 | */ 85 | protected async _type(): Promise { 86 | try { 87 | await this.client.api.post(Routes.channelTyping(this.channel.id)); 88 | } catch { 89 | this.#count = 0; 90 | this._stopTyping(); 91 | } 92 | } 93 | 94 | /** 95 | * Internal method to stop the typing interval if not already stopped. 96 | * @since 0.0.1 97 | */ 98 | protected _stopTyping(): void { 99 | if (this.#interval) { 100 | TimerManager.clearInterval(this.#interval as NodeJS.Timeout); 101 | this.#interval = null; 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/lib/caching/structures/base/Structure.ts: -------------------------------------------------------------------------------- 1 | import { Snowflake } from '@klasa/snowflake'; 2 | import type { Client } from '../../../client/Client'; 3 | 4 | /** 5 | * The base class for Structures 6 | */ 7 | export abstract class Structure { 8 | 9 | /** 10 | * The id to be defined in Structures 11 | */ 12 | public abstract readonly id: string; 13 | 14 | // eslint-disable-next-line no-useless-constructor 15 | public constructor(public readonly client: T) { } 16 | 17 | /** 18 | * The Date when this object was created at 19 | */ 20 | public get createdAt(): Date { 21 | return new Snowflake(this.id).date; 22 | } 23 | 24 | /** 25 | * The time when this object was created at 26 | */ 27 | public get createdTimestamp(): number { 28 | return new Snowflake(this.id).timestamp; 29 | } 30 | 31 | /** 32 | * Basic clone method 33 | */ 34 | public clone(): this { 35 | return Object.assign(Object.create(this), this); 36 | } 37 | 38 | /** 39 | * The method of patching this instance defined in Structures 40 | * @param data The data packet 41 | */ 42 | protected abstract _patch(data: unknown): this; 43 | 44 | /** 45 | * The basic value of this Structure 46 | */ 47 | public valueOf(): string { 48 | return this.id; 49 | } 50 | 51 | /** 52 | * The JSON representation of this object. 53 | */ 54 | public toJSON(): Record { 55 | const returnValue: Record = {}; 56 | for (const [key, value] of Object.entries(this)) if (key !== 'client') Reflect.set(returnValue, key, value?.id ?? value?.toJSON?.() ?? value); 57 | return returnValue; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/caching/structures/channels/CategoryChannel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelType } from '@klasa/dapi-types'; 2 | import { GuildChannel } from './GuildChannel'; 3 | 4 | /** 5 | * @see https://discord.com/developers/docs/resources/channel#channel-object 6 | */ 7 | export class CategoryChannel extends GuildChannel { 8 | 9 | /** 10 | * The type of channel. 11 | * @since 0.0.1 12 | * @see https://discord.com/developers/docs/resources/channel#channel-object-channel-types 13 | */ 14 | public readonly type = ChannelType.GuildCategory; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/caching/structures/channels/Channel.ts: -------------------------------------------------------------------------------- 1 | import { APIChannelPartial, ChannelType, APIChannelData } from '@klasa/dapi-types'; 2 | import { Structure } from '../base/Structure'; 3 | import { extender, ExtenderStructures } from '../../../util/Extender'; 4 | import { Client, ClientEvents } from '../../../client/Client'; 5 | 6 | /** 7 | * @see https://discord.com/developers/docs/resources/channel#channel-object 8 | */ 9 | export abstract class Channel extends Structure { 10 | 11 | /** 12 | * The ID of this channel. 13 | * @since 0.0.1 14 | */ 15 | public readonly id: string; 16 | 17 | /** 18 | * The type of channel. 19 | * @since 0.0.1 20 | * @see https://discord.com/developers/docs/resources/channel#channel-object-channel-types 21 | */ 22 | public readonly abstract type: ChannelType; 23 | 24 | /** 25 | * Whether the DM channel is deleted. 26 | * @since 0.0.1 27 | */ 28 | public deleted = false; 29 | 30 | public constructor(client: Client, data: APIChannelPartial) { 31 | super(client); 32 | this.id = data.id; 33 | this._patch(data); 34 | } 35 | 36 | /** 37 | * Defines toString behavior for channels. 38 | * @since 0.0.1 39 | */ 40 | public toString(): string { 41 | return `<#${this.id}>`; 42 | } 43 | 44 | public static create(client: Client, data: APIChannelData, ...extra: readonly unknown[]): Channel | null { 45 | const existing = client.channels.get(data.id); 46 | if (existing) { 47 | // eslint-disable-next-line dot-notation 48 | existing['_patch'](data); 49 | return existing; 50 | } 51 | 52 | const name = Channel.types.get(data.type); 53 | if (name) return new (extender.get(name))(client, data, ...extra) as Channel; 54 | 55 | client.emit(ClientEvents.Debug, `[Channels] Received data with unknown type '${data.type}'.\n\tPayload: ${JSON.stringify(data)}`); 56 | return null; 57 | } 58 | 59 | private static readonly types = new Map([ 60 | [ChannelType.GuildText, 'TextChannel'], 61 | [ChannelType.DM, 'DMChannel'], 62 | [ChannelType.GuildVoice, 'VoiceChannel'], 63 | [ChannelType.GroupDM, 'Channel'], 64 | [ChannelType.GuildCategory, 'CategoryChannel'], 65 | [ChannelType.GuildNews, 'NewsChannel'], 66 | [ChannelType.GuildStore, 'StoreChannel'] 67 | ]); 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/caching/structures/channels/NewsChannel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelType, APIMessageData, APIChannelFollowResult } from '@klasa/dapi-types'; 2 | import { Routes, RequestOptions } from '@klasa/rest'; 3 | import { GuildTextChannel } from './GuildTextChannel'; 4 | 5 | import type { ChannelModifyOptions } from './GuildChannel'; 6 | import type { Message } from '../messages/Message'; 7 | 8 | /** 9 | * @see https://discord.com/developers/docs/resources/channel#channel-object 10 | */ 11 | export class NewsChannel extends GuildTextChannel { 12 | 13 | /** 14 | * The type of channel. 15 | * @since 0.0.1 16 | * @see https://discord.com/developers/docs/resources/channel#channel-object-channel-types 17 | */ 18 | public readonly type = ChannelType.GuildNews; 19 | 20 | /** 21 | * Crossposts a Message in this channel. 22 | * @param messageID The ID of the {@link Message message} that should be crossposted. 23 | * @since 0.0.1 24 | */ 25 | public async crosspost(messageID: string): Promise { 26 | const messageData = await this.client.api.post(Routes.crosspostMessage(this.id, messageID)) as APIMessageData; 27 | // eslint-disable-next-line dot-notation 28 | return this.messages['_add'](messageData); 29 | } 30 | 31 | /* 32 | * Subscribes a channel to crossposted messages from this channel. 33 | * @param channel The {@link GuildTextChannel channel} that should follow this NewsChannel. 34 | * @since 0.0.4 35 | */ 36 | public async follow(channel: GuildTextChannel): Promise { 37 | // eslint-disable-next-line camelcase 38 | return this.client.api.post(Routes.followChannel(this.id), { data: { webhook_channel_id: channel.id } }) as Promise; 39 | } 40 | 41 | /** 42 | * Modifies this channel. 43 | * @param data The channel modify options. 44 | * @param requestOptions The request options. 45 | * @since 0.0.1 46 | */ 47 | public modify(options: NewsChannelModifyOptions, requestOptions: RequestOptions = {}): Promise { 48 | return super.modify(options, requestOptions); 49 | } 50 | 51 | } 52 | 53 | /* eslint-disable camelcase */ 54 | 55 | export interface NewsChannelModifyOptions extends ChannelModifyOptions { 56 | type?: ChannelType.GuildNews | ChannelType.GuildText; 57 | topic?: string | null; 58 | nsfw?: boolean; 59 | parent_id?: string | null; 60 | } 61 | 62 | /* eslint-enable camelcase */ 63 | -------------------------------------------------------------------------------- /src/lib/caching/structures/channels/StoreChannel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelType, APIChannelData } from '@klasa/dapi-types'; 2 | import { GuildChannel, ChannelModifyOptions } from './GuildChannel'; 3 | 4 | import type { RequestOptions } from '@klasa/rest'; 5 | 6 | /** 7 | * @see https://discord.com/developers/docs/resources/channel#channel-object 8 | */ 9 | export class StoreChannel extends GuildChannel { 10 | 11 | /** 12 | * The type of channel. 13 | * @since 0.0.1 14 | * @see https://discord.com/developers/docs/resources/channel#channel-object-channel-types 15 | */ 16 | public readonly type = ChannelType.GuildStore; 17 | 18 | /** 19 | * Whether or not the channel is nsfw. 20 | * @since 0.0.1 21 | */ 22 | public nsfw!: boolean; 23 | 24 | /** 25 | * Modifies this channel. 26 | * @param data The channel modify options. 27 | * @param requestOptions The request options. 28 | * @since 0.0.1 29 | */ 30 | public modify(options: StoreChannelModifyOptions, requestOptions: RequestOptions = {}): Promise { 31 | return super.modify(options, requestOptions); 32 | } 33 | 34 | protected _patch(data: APIChannelData): this { 35 | this.nsfw = data.nsfw as boolean; 36 | return super._patch(data); 37 | } 38 | 39 | } 40 | 41 | /* eslint-disable camelcase */ 42 | 43 | export interface StoreChannelModifyOptions extends ChannelModifyOptions { 44 | nsfw?: boolean | null; 45 | parent_id?: string | null; 46 | } 47 | 48 | /* eslint-enable camelcase */ 49 | -------------------------------------------------------------------------------- /src/lib/caching/structures/channels/TextChannel.ts: -------------------------------------------------------------------------------- 1 | import { APIChannelData, ChannelType } from '@klasa/dapi-types'; 2 | import { GuildTextChannel } from './GuildTextChannel'; 3 | 4 | import type { ChannelModifyOptions } from './GuildChannel'; 5 | import type { RequestOptions } from '@klasa/rest'; 6 | 7 | /** 8 | * @see https://discord.com/developers/docs/resources/channel#channel-object 9 | */ 10 | export class TextChannel extends GuildTextChannel { 11 | 12 | /** 13 | * The type of channel. 14 | * @since 0.0.1 15 | * @see https://discord.com/developers/docs/resources/channel#channel-object-channel-types 16 | */ 17 | public readonly type = ChannelType.GuildText; 18 | 19 | /** 20 | * Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the 21 | * permission `MANAGE_MESSAGES` or `MANAGE_CHANNEL`, are unaffected. 22 | * @since 0.0.1 23 | */ 24 | public rateLimitPerUser!: number; 25 | 26 | /** 27 | * Modifies this channel. 28 | * @param data The channel modify options. 29 | * @param requestOptions The request options. 30 | * @since 0.0.1 31 | */ 32 | public modify(data: TextChannelModifyOptions, requestOptions: RequestOptions = {}): Promise { 33 | return super.modify(data, requestOptions); 34 | } 35 | 36 | protected _patch(data: APIChannelData): this { 37 | this.rateLimitPerUser = data.rate_limit_per_user as number; 38 | return super._patch(data); 39 | } 40 | 41 | } 42 | 43 | /* eslint-disable camelcase */ 44 | 45 | export interface TextChannelModifyOptions extends ChannelModifyOptions { 46 | type?: ChannelType.GuildText | ChannelType.GuildNews; 47 | topic?: string | null; 48 | nsfw?: boolean | null; 49 | rate_limit_per_user?: number | null; 50 | parent_id?: string | null; 51 | } 52 | 53 | /* eslint-enable camelcase */ 54 | -------------------------------------------------------------------------------- /src/lib/caching/structures/channels/VoiceChannel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelType, APIChannelData } from '@klasa/dapi-types'; 2 | import { GuildChannel, ChannelModifyOptions } from './GuildChannel'; 3 | 4 | import type { RequestOptions } from '@klasa/rest'; 5 | import { Permissions, PermissionsFlags } from '../../../util/bitfields/Permissions'; 6 | 7 | /** 8 | * @see https://discord.com/developers/docs/resources/channel#channel-object 9 | */ 10 | export class VoiceChannel extends GuildChannel { 11 | 12 | /** 13 | * The type of channel. 14 | * @since 0.0.1 15 | * @see https://discord.com/developers/docs/resources/channel#channel-object-channel-types 16 | */ 17 | public readonly type = ChannelType.GuildVoice; 18 | 19 | /** 20 | * The bitrate (in bits) of the voice channel. 21 | * @since 0.0.1 22 | */ 23 | public bitrate!: number; 24 | 25 | /** 26 | * The user limit of the voice channel. 27 | * @since 0.0.1 28 | */ 29 | public userLimit!: number; 30 | 31 | /** 32 | * If the client can delete the channel. 33 | * @since 0.0.1 34 | */ 35 | public get deletable(): boolean | null { 36 | return !this.deleted && this.manageable; 37 | } 38 | 39 | /** 40 | * If the client can manage the channel. 41 | * @since 0.0.1 42 | */ 43 | public get manageable(): boolean | null { 44 | return this.guild.me?.permissionsIn(this).has([ 45 | Permissions.FLAGS[PermissionsFlags.Connect], 46 | Permissions.FLAGS[PermissionsFlags.ViewChannel], 47 | Permissions.FLAGS[PermissionsFlags.ManageChannels] 48 | ]) ?? null; 49 | } 50 | 51 | /** 52 | * Modifies this channel. 53 | * @param data The channel modify options. 54 | * @param requestOptions The request options. 55 | * @since 0.0.1 56 | */ 57 | public modify(data: VoiceChannelModifyOptions, requestOptions: RequestOptions = {}): Promise { 58 | return super.modify(data, requestOptions); 59 | } 60 | 61 | protected _patch(data: APIChannelData): this { 62 | this.bitrate = data.bitrate as number; 63 | this.userLimit = data.user_limit as number; 64 | return super._patch(data); 65 | } 66 | 67 | } 68 | 69 | /* eslint-disable camelcase */ 70 | 71 | export interface VoiceChannelModifyOptions extends ChannelModifyOptions { 72 | bitrate?: number | null; 73 | user_limit?: number | null; 74 | parent_id?: string | null; 75 | } 76 | 77 | /* eslint-enable camelcase */ 78 | -------------------------------------------------------------------------------- /src/lib/caching/structures/guilds/AuditLog.ts: -------------------------------------------------------------------------------- 1 | import type { APIAuditLogData, APIWebhookData, APIUserData, APIAuditLogEntryData, APIIntegrationData } from '@klasa/dapi-types'; 2 | import type { Client } from '../../../client/Client'; 3 | 4 | /** 5 | * @see https://discord.com/developers/docs/resources/audit-log#audit-log-object 6 | */ 7 | export class AuditLog { 8 | 9 | /** 10 | * List of webhooks found in the audit log. 11 | * @since 0.0.1 12 | */ 13 | public webhooks: APIWebhookData[]; 14 | 15 | /** 16 | * List of users found in the audit log. 17 | * @since 0.0.1 18 | */ 19 | public users: APIUserData[]; 20 | 21 | /** 22 | * List of audit log entries. 23 | * @since 0.0.1 24 | */ 25 | public auditLogEntries: APIAuditLogEntryData[]; 26 | 27 | /** 28 | * List of partial integration objects. 29 | * @since 0.0.1 30 | */ 31 | public integrations: Partial[]; 32 | 33 | public constructor(public readonly client: Client, data: APIAuditLogData) { 34 | this.webhooks = data.webhooks; 35 | this.users = data.users; 36 | this.auditLogEntries = data.audit_log_entries; 37 | this.integrations = data.integrations; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/caching/structures/guilds/AuditLogEntry.ts: -------------------------------------------------------------------------------- 1 | import { Structure } from '../base/Structure'; 2 | 3 | import type { APIAuditLogEntryData, APIAuditLogChangeData, AuditLogEvent, APIAuditLogOptionsData } from '@klasa/dapi-types'; 4 | import type { Client } from '../../../client/Client'; 5 | 6 | /** 7 | * @see https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object 8 | */ 9 | export class AuditLogEntry extends Structure { 10 | 11 | /** 12 | * Id of the entry. 13 | * @since 0.0.1 14 | */ 15 | public id: string; 16 | 17 | /** 18 | * Id of the affected entity (webhook, user, role, etc). 19 | * @since 0.0.1 20 | */ 21 | public targetID: string | null; 22 | 23 | /** 24 | * Changes made to the {@link AuditLogEntry#targetID}. 25 | * @since 0.0.1 26 | * @see https://discord.com/developers/docs/resources/audit-log#audit-log-change-object 27 | */ 28 | public changes?: APIAuditLogChangeData[]; 29 | 30 | /** 31 | * The user who made the changes. 32 | * @since 0.0.1 33 | */ 34 | public userID: string; 35 | 36 | /** 37 | * Type of action that occurred. 38 | * @since 0.0.1 39 | * @see https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events 40 | */ 41 | public actionType: AuditLogEvent; 42 | 43 | /** 44 | * Additional info for certain action types. 45 | * @since 0.0.1 46 | * @see https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info 47 | */ 48 | public options?: APIAuditLogOptionsData; 49 | 50 | /** 51 | * The reason for the change (0-512 characters). 52 | * @since 0.0.1 53 | */ 54 | public reason?: string; 55 | 56 | public constructor(client: Client, data: APIAuditLogEntryData) { 57 | super(client); 58 | 59 | this.id = data.id; 60 | this.targetID = data.target_id; 61 | this.changes = data.changes; 62 | this.userID = data.user_id; 63 | this.actionType = data.action_type; 64 | this.options = data.options; 65 | this.reason = data.reason; 66 | } 67 | 68 | protected _patch(): this { 69 | return this; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/caching/structures/guilds/Ban.ts: -------------------------------------------------------------------------------- 1 | import { Structure } from '../base/Structure'; 2 | 3 | import type { RequestOptions } from '@klasa/rest'; 4 | import type { APIBanData } from '@klasa/dapi-types'; 5 | import type { Client } from '../../../client/Client'; 6 | import type { Guild } from './Guild'; 7 | import type { User } from '../User'; 8 | 9 | /** 10 | * @see https://discord.com/developers/docs/resources/guild#ban-object 11 | */ 12 | export class Ban extends Structure { 13 | 14 | /** 15 | * The user's ID that got banned. 16 | * @since 0.0.1 17 | */ 18 | public readonly id: string; 19 | 20 | /** 21 | * The reason for the ban. 22 | * @since 0.0.1 23 | */ 24 | public readonly reason: string | null; 25 | 26 | /** 27 | * The guild this ban is from. 28 | * @since 0.0.1 29 | */ 30 | public readonly guild: Guild; 31 | 32 | /** 33 | * If the ban has been removed. 34 | * @since 0.0.1 35 | */ 36 | public deleted = false; 37 | 38 | public constructor(client: Client, data: APIBanData, guild: Guild) { 39 | super(client); 40 | // eslint-disable-next-line dot-notation 41 | this.id = this.client.users['_add'](data.user).id; 42 | this.reason = data.reason; 43 | this.guild = guild; 44 | } 45 | 46 | /** 47 | * Deletes the ban. (unbans the user) 48 | * @since 0.0.1 49 | * @param requestOptions The additional request options. 50 | * @see https://discord.com/developers/docs/resources/guild#remove-guild-ban 51 | */ 52 | public async delete(requestOptions?: RequestOptions): Promise { 53 | await this.guild.bans.remove(this.id, requestOptions); 54 | this.deleted = true; 55 | return this; 56 | } 57 | 58 | /** 59 | * The user. 60 | * @since 0.0.1 61 | */ 62 | public get user(): User | null { 63 | return this.client.users.get(this.id) ?? null; 64 | } 65 | 66 | protected _patch(): this { 67 | return this; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/caching/structures/guilds/GuildEmoji.ts: -------------------------------------------------------------------------------- 1 | import { Structure } from '../base/Structure'; 2 | 3 | import type { APIEmojiData } from '@klasa/dapi-types'; 4 | import type { RequestOptions } from '@klasa/rest'; 5 | import type { Guild } from './Guild'; 6 | import type { Client } from '../../../client/Client'; 7 | 8 | /** 9 | * @see https://discord.com/developers/docs/resources/emoji#emoji-object 10 | */ 11 | export class GuildEmoji extends Structure { 12 | 13 | /** 14 | * The emoji's ID. 15 | * @since 0.0.1 16 | */ 17 | public readonly id: string; 18 | 19 | /** 20 | * The emoji's name (null only in reaction emoji objects). 21 | * @since 0.0.1 22 | */ 23 | public name!: string | null; 24 | 25 | /** 26 | * The roles this emoji is whitelisted to. 27 | * @since 0.0.1 28 | */ 29 | public roleIDs!: string[]; 30 | 31 | /** 32 | * User that created this emoji. 33 | * @since 0.0.1 34 | */ 35 | public userID!: string | null; 36 | 37 | /** 38 | * Whether or not this emoji must be wrapped in colons. 39 | * @since 0.0.1 40 | */ 41 | public requireColons!: boolean | null; 42 | 43 | /** 44 | * Whether or not this emoji is managed. 45 | * @since 0.0.1 46 | */ 47 | public managed!: boolean | null; 48 | 49 | /** 50 | * Whether this emoji is animated. 51 | * @since 0.0.1 52 | */ 53 | public animated!: boolean | null; 54 | 55 | /** 56 | * Whether or not this emoji can be used, may be false due to loss of Server Boosts. 57 | * @since 0.0.1 58 | */ 59 | public available!: boolean; 60 | 61 | /** 62 | * The guild this emoji is from. 63 | * @since 0.0.1 64 | */ 65 | public readonly guild: Guild; 66 | 67 | /** 68 | * Whether the integration is deleted. 69 | * @since 0.0.1 70 | */ 71 | public deleted = false; 72 | 73 | public constructor(client: Client, data: APIEmojiData, guild: Guild) { 74 | super(client); 75 | this.id = data.id as string; 76 | this.guild = guild; 77 | this._patch(data); 78 | } 79 | 80 | /** 81 | * The identifier to be used for API requests. 82 | * @since 0.0.1 83 | */ 84 | public get identifier(): string { 85 | return this.id ?? encodeURIComponent(this.name as string); 86 | } 87 | 88 | /** 89 | * The emoji as shown in Discord. 90 | * @since 0.0.1 91 | */ 92 | public toString(): string { 93 | return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name as string; 94 | } 95 | 96 | /** 97 | * Deletes an emoji from the {@link Guild guild}. 98 | * @since 0.0.1 99 | * @param emojiID The {@link GuildEmoji guild emoji} ID. 100 | * @param requestOptions The additional request options. 101 | * @see https://discord.com/developers/docs/resources/emoji#create-guild-emoji 102 | */ 103 | public async delete(requestOptions: RequestOptions = {}): Promise { 104 | await this.guild.emojis.remove(this.id, requestOptions); 105 | this.deleted = true; 106 | return this; 107 | } 108 | 109 | protected _patch(data: APIEmojiData): this { 110 | this.animated = data.animated ?? null; 111 | this.managed = data.managed ?? null; 112 | this.name = data.name; 113 | this.requireColons = data.require_colons ?? null; 114 | this.roleIDs = data.roles ?? []; 115 | this.userID = data.user?.id ?? null; 116 | this.available = data.available ?? true; 117 | return this; 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/lib/caching/structures/guilds/Overwrite.ts: -------------------------------------------------------------------------------- 1 | import { Permissions } from '../../../util/bitfields/Permissions'; 2 | import { Structure } from '../base/Structure'; 3 | 4 | import type { APIOverwriteData } from '@klasa/dapi-types'; 5 | import type { Client } from '../../../client/Client'; 6 | import type { GuildChannel } from '../channels/GuildChannel'; 7 | import type { RequestOptions } from '@klasa/rest'; 8 | 9 | export type OverwriteData = Omit; 10 | 11 | /** 12 | * @see https://discord.com/developers/docs/resources/channel#overwrite-object 13 | */ 14 | export class Overwrite extends Structure { 15 | 16 | /** 17 | * A {@link Role} or {@link User} id. 18 | * @since 0.0.1 19 | */ 20 | public readonly id: string; 21 | 22 | /** 23 | * The {@link GuildChannel channel} this is for. 24 | * @since 0.0.1 25 | */ 26 | public readonly channel: GuildChannel; 27 | 28 | /** 29 | * Either "role" or "member". 30 | * @since 0.0.1 31 | */ 32 | public type!: 'role' | 'member'; 33 | 34 | /** 35 | * The allowed permissions in this overwrite. 36 | * @since 0.0.1 37 | */ 38 | public allow!: Readonly; 39 | 40 | /** 41 | * The denied permissions in this overwrite. 42 | * @since 0.0.1 43 | */ 44 | public deny!: Readonly; 45 | 46 | /** 47 | * If the overwrite has been deleted. 48 | * @since 0.0.1 49 | */ 50 | public deleted = false; 51 | 52 | public constructor(client: Client, data: APIOverwriteData, channel: GuildChannel) { 53 | super(client); 54 | this.id = data.id; 55 | this.type = data.type; 56 | this.channel = channel; 57 | this._patch(data); 58 | } 59 | 60 | /** 61 | * Deletes this overwrite. 62 | * @param requestOptions The additional request options. 63 | */ 64 | public async delete(requestOptions: RequestOptions = {}): Promise { 65 | await this.channel.permissionOverwrites.remove(this.id, requestOptions); 66 | return this; 67 | } 68 | 69 | /** 70 | * Modifies this overwrite. 71 | * @param options The modify options 72 | * @param requestOptions The additional request options. 73 | */ 74 | public async modify(options: Partial, requestOptions: RequestOptions = {}): Promise { 75 | const data = { 76 | type: this.type, 77 | allow: options.allow ?? this.allow.bitfield, 78 | deny: options.deny ?? this.deny.bitfield 79 | }; 80 | await this.channel.permissionOverwrites.add(this.id, data, requestOptions); 81 | return this._patch(data); 82 | } 83 | 84 | protected _patch(data: APIOverwriteData | OverwriteData): this { 85 | this.allow = new Permissions(data.allow).freeze(); 86 | this.deny = new Permissions(data.deny).freeze(); 87 | return this; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/lib/caching/structures/guilds/Presence.ts: -------------------------------------------------------------------------------- 1 | import { Structure } from '../base/Structure'; 2 | 3 | import type { APIPresenceUpdateData, PresenceUpdateStatus, APIActivityData, APIClientStatusData } from '@klasa/dapi-types'; 4 | import type { Client } from '../../../client/Client'; 5 | 6 | /** 7 | * @see https://discord.com/developers/docs/topics/gateway#presence 8 | */ 9 | export class Presence extends Structure { 10 | 11 | /** 12 | * The member's ID this presence corresponds to. 13 | * @since 0.0.1 14 | */ 15 | public readonly id: string; 16 | 17 | /** 18 | * The member's status. 19 | * @since 0.0.1 20 | */ 21 | public status!: PresenceUpdateStatus | null; 22 | 23 | /** 24 | * The member's platform-dependent status. 25 | * @since 0.0.1 26 | */ 27 | public clientStatus!: APIClientStatusData | null; 28 | 29 | /** 30 | * The member's current activities. 31 | * @since 0.0.1 32 | */ 33 | public activities!: APIActivityData[]; 34 | 35 | public constructor(client: Client, data: APIPresenceUpdateData) { 36 | super(client); 37 | this.id = data.user.id; 38 | this._patch(data); 39 | } 40 | 41 | protected _patch(data: APIPresenceUpdateData): this { 42 | this.status = data.status ?? null; 43 | this.clientStatus = data.client_status ?? null; 44 | this.activities = data.activities ?? []; 45 | return this; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/caching/structures/guilds/VoiceState.ts: -------------------------------------------------------------------------------- 1 | import { Structure } from '../base/Structure'; 2 | 3 | import type { APIVoiceStatePartial } from '@klasa/dapi-types'; 4 | import type { Client } from '../../../client/Client'; 5 | import type { Guild } from './Guild'; 6 | 7 | /** 8 | * @see https://discord.com/developers/docs/resources/voice#voice-state-object 9 | */ 10 | export class VoiceState extends Structure { 11 | 12 | /** 13 | * The user id this voice state is for. 14 | * @since 0.0.1 15 | */ 16 | public readonly id: string; 17 | 18 | /** 19 | * The channel id this user is connected to. 20 | * @since 0.0.1 21 | */ 22 | public channelID!: string | null; 23 | 24 | /** 25 | * The voice state's session id. 26 | * @since 0.0.1 27 | */ 28 | public sessionID!: string; 29 | 30 | /** 31 | * Whether or not this user is deafened by the server. 32 | * @since 0.0.1 33 | */ 34 | public deaf!: boolean; 35 | 36 | /** 37 | * Whether or not this user is muted by the server. 38 | * @since 0.0.1 39 | */ 40 | public mute!: boolean; 41 | 42 | /** 43 | * Whether or not this user is locally deafened. 44 | * @since 0.0.1 45 | */ 46 | public selfDeaf!: boolean; 47 | 48 | /** 49 | * Whether or not this user is locally muted. 50 | * @since 0.0.1 51 | */ 52 | public selfMute!: boolean; 53 | 54 | /** 55 | * Whether or not this user is streaming using "Go Live". 56 | * @since 0.0.1 57 | */ 58 | public selfStream!: boolean | null; 59 | 60 | /** 61 | * Whether or not this user is muted by the current user. 62 | * @since 0.0.1 63 | */ 64 | public suppress!: boolean; 65 | 66 | public readonly guild: Guild; 67 | 68 | public constructor(client: Client, data: APIVoiceStatePartial, guild: Guild) { 69 | super(client); 70 | this.id = data.user_id; 71 | this.guild = guild; 72 | this._patch(data); 73 | } 74 | 75 | protected _patch(data: APIVoiceStatePartial): this { 76 | this.channelID = data.channel_id; 77 | this.sessionID = data.session_id; 78 | this.deaf = data.deaf; 79 | this.mute = data.mute; 80 | this.selfDeaf = data.self_deaf; 81 | this.selfMute = data.self_mute; 82 | this.selfStream = data.self_stream ?? null; 83 | this.suppress = data.suppress; 84 | return this; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/caching/structures/messages/MessageAttachment.ts: -------------------------------------------------------------------------------- 1 | import type { APIMessageAttachmentData } from '@klasa/dapi-types'; 2 | 3 | /* eslint-disable camelcase */ 4 | 5 | /** 6 | * @see https://discord.com/developers/docs/resources/channel#attachment-object 7 | */ 8 | export class MessageAttachment implements APIMessageAttachmentData { 9 | 10 | /** 11 | * Attachment id. 12 | * @since 0.0.1 13 | */ 14 | public readonly id: string; 15 | 16 | /** 17 | * Name of file attached. 18 | * @since 0.0.1 19 | */ 20 | public filename: string; 21 | 22 | /** 23 | * Size of file in bytes. 24 | * @since 0.0.1 25 | */ 26 | public size: number; 27 | 28 | /** 29 | * Source url of file. 30 | * @since 0.0.1 31 | */ 32 | public url: string; 33 | 34 | /** 35 | * A proxied url of file. 36 | * @since 0.0.1 37 | */ 38 | public proxy_url: string; 39 | 40 | /** 41 | * Height of file (if image). 42 | * @since 0.0.1 43 | */ 44 | public height: number | null; 45 | 46 | /** 47 | * Width of file (if image). 48 | * @since 0.0.1 49 | */ 50 | public width: number | null; 51 | 52 | public constructor(attachment: APIMessageAttachmentData) { 53 | this.id = attachment.id; 54 | 55 | this.filename = attachment.filename; 56 | 57 | this.size = attachment.size; 58 | 59 | this.url = attachment.url; 60 | 61 | // eslint-disable-next-line camelcase 62 | this.proxy_url = attachment.proxy_url; 63 | 64 | this.height = attachment.height || null; 65 | 66 | this.width = attachment.width || null; 67 | } 68 | 69 | } 70 | 71 | /* eslint-enable camelcase */ 72 | -------------------------------------------------------------------------------- /src/lib/caching/structures/messages/MessageMentions.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@klasa/cache'; 2 | 3 | import type { Message } from './Message'; 4 | import type { APIMessageMentionChannelData, APIChannelData, APIMessageMentionData } from '@klasa/dapi-types'; 5 | import type { User } from '../User'; 6 | import type { Guild } from '../guilds/Guild'; 7 | 8 | export class MessageMentions { 9 | 10 | /** 11 | * The {@link Message} this entry belongs to. 12 | * @since 0.0.1 13 | */ 14 | public readonly message: Message; 15 | 16 | /** 17 | * Users specifically mentioned in the message. 18 | * @since 0.0.1 19 | */ 20 | public readonly users: Cache; 21 | 22 | /** 23 | * Roles specifically mentioned in this message. 24 | * @since 0.0.1 25 | */ 26 | public readonly roles: Cache; 27 | 28 | /** 29 | * Channels specifically mentioned in this message. 30 | * @since 0.0.1 31 | */ 32 | public readonly channels: Cache; 33 | 34 | /** 35 | * Whether this message mentions everyone. 36 | * @since 0.0.1 37 | */ 38 | public readonly everyone: boolean; 39 | 40 | public constructor(message: Message, users: APIMessageMentionData[], roles: string[], channels: APIMessageMentionChannelData[], everyone: boolean) { 41 | this.message = message; 42 | this.users = new Cache(); 43 | this.roles = new Cache(); 44 | this.channels = new Cache(); 45 | 46 | if (users) { 47 | for (const mention of users) { 48 | // eslint-disable-next-line dot-notation 49 | const user = this.message.client.users['_add'](mention); 50 | this.users.set(user.id, user); 51 | 52 | if (mention.member) { 53 | // eslint-disable-next-line dot-notation 54 | (this.message.guild as Guild).members['_add']({ ...mention.member, user }); 55 | } 56 | } 57 | } 58 | 59 | // Just for now why there is no role store setup 60 | if (roles) for (const role of roles) this.roles.set(role, role); 61 | if (channels) for (const mention of channels) this.channels.set(mention.id, mention); 62 | 63 | this.everyone = Boolean(everyone); 64 | } 65 | 66 | public toJSON(): Record { 67 | return { 68 | message: this.message.id, 69 | users: [...this.users.keys()], 70 | roles: [...this.roles.keys()], 71 | channels: [...this.channels.keys()], 72 | everyone: this.everyone 73 | }; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/caching/structures/messages/WebhookMessage.ts: -------------------------------------------------------------------------------- 1 | import { Structure } from '../base/Structure'; 2 | import { extender } from '../../../util/Extender'; 3 | 4 | import type { APIMessageData, MessageType } from '@klasa/dapi-types'; 5 | import type { Embed } from '../Embed'; 6 | import type { User } from '../User'; 7 | import type { WebhookClient } from '../../../client/WebhookClient'; 8 | 9 | export class WebhookMessage extends Structure { 10 | 11 | /** 12 | * Id of the message. 13 | * @since 0.0.1 14 | */ 15 | public readonly id: string; 16 | 17 | /** 18 | * Author of this message. 19 | * @since 0.0.1 20 | */ 21 | public readonly author: User; 22 | 23 | /** 24 | * Contents of the message. 25 | * @since 0.0.1 26 | */ 27 | public content: string; 28 | 29 | /** 30 | * Whether or not this was a TTS message. 31 | * @since 0.0.1 32 | */ 33 | public tts!: boolean; 34 | 35 | /** 36 | * The embedded data. 37 | * @since 0.0.1 38 | */ 39 | public embeds: Embed[] = []; 40 | 41 | /** 42 | * Used for validating a message was sent. 43 | * @since 0.0.1 44 | */ 45 | public readonly nonce?: string | null; 46 | 47 | /** 48 | * If the message is generated by a webhook, this is the webhook's id. 49 | * @since 0.0.1 50 | */ 51 | public readonly webhookID?: string | null; 52 | 53 | /** 54 | * The type of message. 55 | * @since 0.0.1 56 | * @see https://discord.com/developers/docs/resources/channel#message-object-message-types 57 | */ 58 | public readonly type: MessageType; 59 | 60 | public constructor(client: T, data: APIMessageData) { 61 | super(client); 62 | this.id = data.id; 63 | this.content = data.content; 64 | // eslint-disable-next-line dot-notation 65 | this.author = new (extender.get('User'))(client, data.author) as unknown as User; 66 | this.type = data.type; 67 | 68 | this.nonce = data.nonce ?? null; 69 | this.webhookID = data.webhook_id ?? null; 70 | } 71 | 72 | /** 73 | * When this message was sent. 74 | * @since 0.0.1 75 | */ 76 | public get createdAt(): Date { 77 | return new Date(this.createdTimestamp); 78 | } 79 | 80 | /** 81 | * Defines the toString behavior of this structure. 82 | * @since 0.0.4 83 | */ 84 | public toString(): string { 85 | return this.content; 86 | } 87 | 88 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 89 | protected _patch(_data: Partial): this { 90 | return this; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/caching/structures/messages/WebhookMessageBuilder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { mergeDefault, RequiredExcept, PartialRequired } from '@klasa/utils'; 3 | 4 | import type { File, RequestOptions } from '@klasa/rest'; 5 | import type { APIEmbedData } from '@klasa/dapi-types'; 6 | 7 | import { MessageBuilder, AllowedMentions, SplitOptions } from './MessageBuilder'; 8 | import { Embed } from '../Embed'; 9 | 10 | /* eslint-disable camelcase */ 11 | 12 | export interface WebhookMessageData { 13 | content?: string; 14 | embeds?: APIEmbedData[]; 15 | nonce?: string; 16 | tts?: boolean; 17 | username?: string; 18 | avatar_url?: string; 19 | allowed_mentions?: Required; 20 | } 21 | 22 | export interface WebhookMessageOptions extends RequestOptions { 23 | data?: WebhookMessageData; 24 | } 25 | 26 | /* eslint-enable camelcase */ 27 | 28 | export class WebhookMessageBuilder extends MessageBuilder implements RequiredExcept { 29 | 30 | /** 31 | * The Webhook Message data to send to the api 32 | */ 33 | public data: PartialRequired; 34 | 35 | /** 36 | * The files to send to the api 37 | */ 38 | public files: File[]; 39 | 40 | /** 41 | * Webhook messages don't use auth 42 | */ 43 | public auth = false; 44 | 45 | /** 46 | * @param webhookMessageOptions The options to create this 47 | */ 48 | public constructor({ data = {}, files = [] }: WebhookMessageOptions = {}) { 49 | super(); 50 | const defaultedData = mergeDefault({ 51 | // eslint-disable-next-line camelcase 52 | allowed_mentions: { 53 | parse: [] as ('users' | 'roles' | 'everyone')[], 54 | roles: [] as string[], 55 | users: [] as string[] 56 | } 57 | } as PartialRequired, data); 58 | 59 | this.data = defaultedData; 60 | this.files = files; 61 | } 62 | 63 | /** 64 | * WebhookMessages have multiple embeds, use the addEmbed or spliceEmbed methods instead. 65 | */ 66 | public setEmbed(): never { 67 | throw new Error('WebhookMessages have multiple embeds, use the addEmbed or spliceEmbed methods instead.'); 68 | } 69 | 70 | /** 71 | * Adds an embed to this webhook message 72 | * @param embed The field name 73 | */ 74 | public addEmbed(embed: APIEmbedData): this 75 | public addEmbed(embed: (embed: Embed) => Embed): this 76 | public addEmbed(embed: APIEmbedData | ((embed: Embed) => Embed)): this { 77 | if (!this.data.embeds) this.data.embeds = []; 78 | this.data.embeds.push(typeof embed === 'function' ? embed(new Embed()) : embed); 79 | return this; 80 | } 81 | 82 | /** 83 | * Deletes and/or inserts embeds by index in the webhook message 84 | * @param index The index to start at 85 | * @param deleteCount How many fields to delete 86 | * @param embed The field name to insert 87 | */ 88 | public spliceEmbed(index: number, deleteCount: number, embed?: APIEmbedData): this 89 | public spliceEmbed(index: number, deleteCount: number, embed?: (embed: Embed) => Embed): this 90 | public spliceEmbed(index: number, deleteCount: number, embed?: APIEmbedData | ((embed: Embed) => Embed)): this { 91 | if (!this.data.embeds) this.data.embeds = []; 92 | if (embed) this.data.embeds.splice(index, deleteCount, typeof embed === 'function' ? embed(new Embed()) : embed); 93 | else this.data.embeds.splice(index, deleteCount); 94 | return this; 95 | } 96 | 97 | /** 98 | * Sets the username of the webhook message 99 | * @param username The username of the webhook message 100 | */ 101 | public setUsername(username?: string): this { 102 | this.data.username = username; 103 | return this; 104 | } 105 | 106 | /** 107 | * Sets the avatar of the webhook message 108 | * @param avatar The avatar for the webhook message 109 | */ 110 | public setAvatar(avatar?: string): this { 111 | // eslint-disable-next-line camelcase 112 | this.data.avatar_url = avatar; 113 | return this; 114 | } 115 | 116 | /** 117 | * Splits this into multiple messages. 118 | * @param options Options to split the message by. 119 | */ 120 | public split(options: SplitOptions = {}): RequestOptions[] { 121 | // If there isn't content, the message can't be split 122 | if (!this.data.content) return [this]; 123 | 124 | const messages = this._split(options); 125 | 126 | // Don't send any possible empty messages, and return the array of RequestOptions 127 | return messages.filter(mes => mes).map((content, index) => index === 0 ? 128 | // first message has embed/s and files 129 | { data: { ...this.data, content }, files: this.files } : 130 | // Later messages have neither 131 | { data: { ...this.data, content, embeds: null } }); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/lib/caching/structures/messages/reactions/MessageReaction.ts: -------------------------------------------------------------------------------- 1 | import { MessageReactionEmoji } from './MessageReactionEmoji'; 2 | import { Structure } from '../../base/Structure'; 3 | import { MessageReactionUserStore } from '../../../stores/MessageReactionUserStore'; 4 | 5 | import type { APIReactionData } from '@klasa/dapi-types'; 6 | import type { Message } from '../Message'; 7 | import type { Client } from '../../../../client/Client'; 8 | 9 | /** 10 | * @see https://discord.com/developers/docs/resources/channel#reaction-object 11 | */ 12 | export class MessageReaction extends Structure { 13 | 14 | /** 15 | * The reaction ID. 16 | * @since 0.0.1 17 | */ 18 | public readonly id: string; 19 | 20 | /** 21 | * Whether or not the current user reacted using this emoji. 22 | * @since 0.0.1 23 | */ 24 | public me!: boolean; 25 | 26 | /** 27 | * Times this emoji has been used to react. 28 | * @since 0.0.1 29 | */ 30 | public count!: number; 31 | 32 | /** 33 | * Emoji information. 34 | * @since 0.0.1 35 | */ 36 | public readonly emoji: MessageReactionEmoji; 37 | 38 | /** 39 | * The users that reacted to this emoji. 40 | * @since 0.0.1 41 | */ 42 | public readonly users: MessageReactionUserStore; 43 | 44 | /** 45 | * The {@link Message message} instance this is tied to. 46 | * @since 0.0.1 47 | */ 48 | public readonly message: Message; 49 | 50 | public constructor(client: Client, data: APIReactionData, message: Message) { 51 | super(client); 52 | this.id = data.emoji.id ?? data.emoji.name as string; 53 | this.message = message; 54 | this.emoji = new MessageReactionEmoji(client, data.emoji); 55 | this.users = new MessageReactionUserStore(this); 56 | this._patch(data); 57 | } 58 | 59 | /** 60 | * The emoji as shown in Discord. 61 | * @since 0.0.1 62 | */ 63 | public toString(): string { 64 | return this.emoji.toString(); 65 | } 66 | 67 | /** 68 | * Defines the JSON.stringify behavior of this structure. 69 | * @since 0.0.1 70 | */ 71 | public toJSON(): Record { 72 | return { 73 | me: this.me, 74 | count: this.count, 75 | emoji: this.emoji.toJSON() 76 | }; 77 | } 78 | 79 | protected _patch(data: APIReactionData): this { 80 | this.me = data.me; 81 | this.count = data.count; 82 | return this; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/lib/caching/structures/messages/reactions/MessageReactionEmoji.ts: -------------------------------------------------------------------------------- 1 | import type { APIEmojiPartial } from '@klasa/dapi-types'; 2 | import type { Client } from '../../../../client/Client'; 3 | 4 | /** 5 | * @see https://discord.com/developers/docs/resources/emoji#emoji-object 6 | */ 7 | export class MessageReactionEmoji implements APIEmojiPartial { 8 | 9 | /** 10 | * The emoji's ID. 11 | * @since 0.0.1 12 | */ 13 | public readonly id: string | null; 14 | 15 | /** 16 | * Emoji name. 17 | * @since 0.0.1 18 | */ 19 | public readonly name: string | null; 20 | 21 | /** 22 | * Whether this emoji is animated. 23 | * @since 0.0.1 24 | */ 25 | public readonly animated: boolean; 26 | 27 | public constructor(public readonly client: Client, data: APIEmojiPartial) { 28 | this.id = data.id; 29 | this.name = data.name; 30 | this.animated = data.animated ?? false; 31 | } 32 | 33 | /** 34 | * The identifier to be used for API requests. 35 | * @since 0.0.1 36 | */ 37 | public get identifier(): string { 38 | return this.id ?? encodeURIComponent(this.name as string); 39 | } 40 | 41 | /** 42 | * The emoji as shown in Discord. 43 | * @since 0.0.1 44 | */ 45 | public toString(): string { 46 | return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name as string; 47 | } 48 | 49 | /** 50 | * Defines the JSON.stringify behavior of this structure. 51 | * @since 0.0.1 52 | */ 53 | public toJSON(): Record { 54 | return { 55 | id: this.id, 56 | name: this.name, 57 | animated: this.animated 58 | }; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/caching/structures/oauth/Team.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@klasa/cache'; 2 | import { TeamMember } from './TeamMember'; 3 | 4 | import type { APITeamData } from '@klasa/dapi-types'; 5 | import type { ImageURLOptions } from '@klasa/rest'; 6 | import type { Client } from '../../../client/Client'; 7 | 8 | /** 9 | * @see https://discord.com/developers/docs/topics/teams#data-models-team-object 10 | */ 11 | export class Team { 12 | 13 | /** 14 | * The unique id of the team. 15 | * @since 0.0.1 16 | */ 17 | public readonly id: string; 18 | 19 | /** 20 | * A hash of the image of the team's icon. 21 | * @since 0.0.1 22 | */ 23 | public icon?: string; 24 | 25 | /** 26 | * The members of the team. 27 | * @since 0.0.1 28 | */ 29 | public members: Cache; 30 | 31 | /** 32 | * The user id of the current team owner. 33 | * @since 0.0.1 34 | */ 35 | public ownerID: string; 36 | 37 | public constructor(public readonly client: Client, data: APITeamData) { 38 | this.id = data.id; 39 | this.icon = data.icon; 40 | this.members = new Cache(data.members.map(member => [member.user.id, new TeamMember(client, member)])); 41 | this.ownerID = data.owner_user_id; 42 | } 43 | 44 | /** 45 | * The owner of this Team 46 | * @since 0.0.4 47 | */ 48 | public get owner(): TeamMember { 49 | return this.members.get(this.ownerID) as TeamMember; 50 | } 51 | 52 | /** 53 | * Returns the teams icon url if available. 54 | * @param options The image size, format, and other image url options. 55 | */ 56 | public iconURL(options?: ImageURLOptions): string | null { 57 | return this.icon ? this.client.api.cdn.teamIcon(this.id, this.icon, options) : null; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/caching/structures/oauth/TeamMember.ts: -------------------------------------------------------------------------------- 1 | import type { APITeamMember, TeamMembershipState } from '@klasa/dapi-types'; 2 | import type { Client } from '../../../client/Client'; 3 | import type { User } from '../User'; 4 | 5 | /** 6 | * @see https://discord.com/developers/docs/topics/teams#data-models-team-members-object 7 | */ 8 | export class TeamMember { 9 | 10 | /** 11 | * The user's membership state on the team. 12 | * @since 0.0.1 13 | */ 14 | public membershipState: TeamMembershipState; 15 | 16 | /** 17 | * Will always be ["*"]. 18 | * @since 0.0.1 19 | */ 20 | public permissions: string[]; 21 | 22 | /** 23 | * The {@link User} this represents. 24 | * @since 0.0.4 25 | */ 26 | public user: User; 27 | 28 | public constructor(public readonly client: Client, data: APITeamMember) { 29 | this.membershipState = data.membership_state; 30 | this.permissions = data.permissions; 31 | // eslint-disable-next-line dot-notation 32 | this.user = this.client.users['_add'](data.user); 33 | } 34 | 35 | /** 36 | * The {@link User} ID. 37 | * @since 0.0.1 38 | */ 39 | public get id(): string { 40 | return this.user.id; 41 | } 42 | 43 | /** 44 | * Defines toString behavior for team members. 45 | * @since 0.0.1 46 | */ 47 | public toString(): string { 48 | return this.user.toString(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/caching/structures/presences/ClientPresence.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { OpCodes, SendPayload, PresenceUpdateData } from '@klasa/ws'; 3 | import { Presence } from '../guilds/Presence'; 4 | import { PresenceBuilder } from './PresenceBuilder'; 5 | 6 | /** 7 | * The {@link Presence presence} for the {@link ClientUser client user}. 8 | * @since 0.0.1 9 | * @see https://discord.com/developers/docs/topics/gateway#presence 10 | */ 11 | export class ClientPresence extends Presence { 12 | 13 | /** 14 | * Sets the client presence. 15 | * @since 0.0.1 16 | * @param presence The presence data to be sent. 17 | * @see https://discord.com/developers/docs/topics/gateway#update-status 18 | */ 19 | public modify(game: PresenceUpdateData, shards?: number | number[]): this; 20 | /** 21 | * Sets the client presence with a builder, and returns it. 22 | * @since 0.0.1 23 | * @param builder The builder to aid building the game. 24 | */ 25 | public modify(builder: (presence: PresenceBuilder) => PresenceBuilder, shards?: number | number[]): this; 26 | public modify(presence: PresenceUpdateData | ((game: PresenceBuilder) => PresenceBuilder), shards?: number | number[]): this { 27 | const data = typeof presence === 'function' ? presence(new PresenceBuilder()) : presence; 28 | // eslint-disable-next-line id-length 29 | const sent: SendPayload = { op: OpCodes.STATUS_UPDATE, d: data }; 30 | 31 | // No shards specified 32 | if (typeof shards === 'undefined') { 33 | for (const shard of this.client.ws.shards.values()) shard.send(sent); 34 | return this; 35 | } 36 | 37 | // One shard specified 38 | if (typeof shards === 'number') { 39 | const shard = this.client.ws.shards.get(shards); 40 | if (shard) shard.send(sent); 41 | return this; 42 | } 43 | 44 | // Multiple shards specified 45 | for (const shardID of shards) { 46 | const shard = this.client.ws.shards.get(shardID); 47 | if (shard) shard.send(sent); 48 | } 49 | 50 | return this; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/caching/structures/presences/PresenceBuilder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { PresenceGameBuilder } from './PresenceGameBuilder'; 3 | 4 | import type { PresenceUpdateData } from '@klasa/ws'; 5 | import type { APIActivityData } from '@klasa/dapi-types'; 6 | 7 | export type PresenceStatus = PresenceUpdateData['status']; 8 | 9 | /** 10 | * The presence builder. 11 | * @since 0.0.1 12 | * @see https://discord.com/developers/docs/topics/gateway#update-status-gateway-status-update-structure 13 | */ 14 | export class PresenceBuilder implements PresenceUpdateData { 15 | 16 | /** 17 | * Unix time (in milliseconds) of when the client went idle, or null if the client is not idle. 18 | * @since 0.0.1 19 | */ 20 | public since: number | null; 21 | 22 | /** 23 | * Null, or the user's new activity. 24 | * @since 0.0.1 25 | * @see https://discord.com/developers/docs/topics/gateway#activity-object 26 | */ 27 | public game: APIActivityData | null; 28 | 29 | /** 30 | * The user's new status. 31 | * @since 0.0.1 32 | * @see https://discord.com/developers/docs/topics/gateway#update-status-status-types 33 | */ 34 | public status: PresenceStatus; 35 | 36 | /** 37 | * Whether or not the client is afk. 38 | * @since 0.0.1 39 | */ 40 | public afk: boolean; 41 | 42 | public constructor(data: Partial = {}) { 43 | this.since = data.since ?? null; 44 | this.game = data.game ?? null; 45 | this.status = data.status ?? 'online'; 46 | this.afk = data.afk ?? false; 47 | } 48 | 49 | /** 50 | * Modifies the presence and returns it. 51 | * @since 0.0.1 52 | * @param since Unix time (in milliseconds) of when the client went idle, or null if the client is not idle. 53 | */ 54 | public setSince(since: Date | number | null = Date.now()): this { 55 | this.since = since instanceof Date ? since.getTime() : since; 56 | return this; 57 | } 58 | 59 | /** 60 | * Modifies the presence with raw data, and returns it. 61 | * @param game Null, or the user's new activity. 62 | * @see https://discord.com/developers/docs/topics/gateway#activity-object 63 | */ 64 | public setGame(game: PresenceGameBuilder | null): this; 65 | /** 66 | * Modifies the presence with a builder, and returns it. 67 | * @since 0.0.1 68 | * @param builder The builder to aid building the game. 69 | */ 70 | public setGame(game: (game: PresenceGameBuilder) => PresenceGameBuilder): this; 71 | public setGame(game: APIActivityData | null | ((game: PresenceGameBuilder) => PresenceGameBuilder)): this { 72 | this.game = typeof game === 'function' ? game(new PresenceGameBuilder()) : game; 73 | return this; 74 | } 75 | 76 | /** 77 | * Modifies the presence and returns it. 78 | * @since 0.0.1 79 | * @param status The user's new status. 80 | * @see https://discord.com/developers/docs/topics/gateway#update-status-status-types 81 | */ 82 | public setStatus(status: PresenceStatus): this { 83 | this.status = status; 84 | return this; 85 | } 86 | 87 | /** 88 | * Modifies the presence and returns it. 89 | * @since 0.0.1 90 | * @param afk Whether or not the client is afk. 91 | */ 92 | public setAfk(afk: boolean): this { 93 | this.afk = afk; 94 | return this; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/client/BaseClient.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { mergeDefault } from '@klasa/utils'; 3 | import { REST, RESTOptions, RESTManagerEvents } from '@klasa/rest'; 4 | import { TimerManager } from '@klasa/timer-manager'; 5 | import { BaseClientOptionsDefaults } from '../util/Constants'; 6 | import { ClientEvents } from '../client/Client'; 7 | 8 | export interface BaseClientOptions { 9 | rest: Partial; 10 | } 11 | 12 | /** 13 | * The Klasa-Core Base Client used to wrap the Discord API 14 | */ 15 | export class BaseClient extends EventEmitter { 16 | 17 | /** 18 | * The rest api interface 19 | */ 20 | public api: REST; 21 | 22 | /** 23 | * The options to use for this client 24 | */ 25 | public options: BaseClientOptions; 26 | 27 | /** 28 | * @param options All of your preferences on how Klasa-Core should work for you 29 | */ 30 | public constructor(options: Partial = {}) { 31 | super(); 32 | this.options = mergeDefault(BaseClientOptionsDefaults, options); 33 | this.api = new REST(this.options.rest) 34 | .on(RESTManagerEvents.Debug, this.emit.bind(this, ClientEvents.RESTDebug)) 35 | .on(RESTManagerEvents.Ratelimited, this.emit.bind(this, ClientEvents.Ratelimited)); 36 | } 37 | 38 | /** 39 | * Sets the token to use for the api. 40 | */ 41 | set token(token: string) { 42 | this.api.token = token; 43 | } 44 | 45 | /** 46 | * Destroys all timers 47 | */ 48 | public async destroy(): Promise { 49 | TimerManager.destroy(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/client/WebhookClient.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient } from './BaseClient'; 2 | import { Webhook } from '../caching/structures/Webhook'; 3 | import { Cache } from '@klasa/cache'; 4 | 5 | /** 6 | * The Klasa-Core Webhook Client used to manipulate webhooks 7 | */ 8 | export class WebhookClient extends BaseClient { 9 | 10 | /** 11 | * Cache of all fetched webhooks 12 | */ 13 | public webhooks: Cache = new Cache(); 14 | 15 | /** 16 | * Fetches a webhook from the api 17 | * @param id The webhook id 18 | * @param token The webhook token 19 | */ 20 | public async fetch(id: string, token?: string): Promise { 21 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 22 | // @ts-expect-error 23 | const webhook = await Webhook.fetch(this, id, token); 24 | this.webhooks.set(id, webhook); 25 | return webhook; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/pieces/Action.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventOptions } from './Event'; 2 | import { snakeToCamel } from '../util/Util'; 3 | 4 | import type { ActionStore } from './ActionStore'; 5 | import type { EventStore } from './EventStore'; 6 | import type { DispatchPayload } from '@klasa/ws'; 7 | import type { Structure } from '../caching/structures/base/Structure'; 8 | 9 | /** 10 | * The common class for all actions. 11 | */ 12 | export abstract class Action extends Event { 13 | 14 | /** 15 | * The name of the event that the {@link Client} will fire. 16 | * @since 0.0.1 17 | */ 18 | public readonly clientEvent: string; 19 | 20 | /** 21 | * @since 0.0.1 22 | * @param store The store this piece is for 23 | * @param directory The base directory to the pieces folder 24 | * @param file The path from the pieces folder to the piece file 25 | * @param options The options for this piece 26 | */ 27 | public constructor(store: ActionStore, directory: string, file: readonly string[], options: ActionOptions = {}) { 28 | super(store as unknown as EventStore, directory, file, { ...options, once: false, emitter: 'ws' }); 29 | this.clientEvent = options.clientEvent ?? snakeToCamel(this.event); 30 | } 31 | 32 | /** 33 | * Processes the event data from the websocket. 34 | * @since 0.0.1 35 | * @param data The raw data from {@link Client#ws} 36 | */ 37 | public run(data: T): unknown { 38 | const struct = this.check(data); 39 | if (struct) { 40 | const previous = struct.clone(); 41 | // eslint-disable-next-line dot-notation 42 | struct['_patch'](data.d); 43 | 44 | // We emit the patched then the previous data so created events, which 45 | // will always fail in this check, consistently emit the new data as 46 | // first argument. 47 | this.client.emit(this.clientEvent, struct, previous); 48 | return; 49 | } 50 | 51 | const built = this.build(data); 52 | if (built) { 53 | this.cache(built); 54 | this.client.emit(this.clientEvent, built); 55 | } 56 | } 57 | 58 | /** 59 | * Checks whether or not the data structure was already cached, returning it if it was. 60 | * @since 0.0.1 61 | * @param data The raw data from {@link Client#ws} 62 | */ 63 | public abstract check(data: T): S | null; 64 | 65 | /** 66 | * Builds the structure from raw data. 67 | * @param data The raw data from {@link Client#ws} 68 | */ 69 | public abstract build(data: T): S | null; 70 | 71 | /** 72 | * Stores the data into the cache. 73 | * @param data The build structure from {@link Action#build} to be cached 74 | */ 75 | public abstract cache(data: S): void; 76 | 77 | } 78 | 79 | /** 80 | * The piece options for all {@link Action} instances. 81 | */ 82 | export interface ActionOptions extends EventOptions { 83 | /** 84 | * The name of the event from {@link Client} to be fired. 85 | */ 86 | clientEvent?: string; 87 | 88 | /** 89 | * @internal 90 | */ 91 | once?: never; 92 | 93 | /** 94 | * @internal 95 | */ 96 | emitter?: never; 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/pieces/ActionStore.ts: -------------------------------------------------------------------------------- 1 | import { Store, PieceConstructor } from './base/Store'; 2 | import { Action } from './Action'; 3 | 4 | import type { Client } from '../client/Client'; 5 | 6 | /** 7 | * @since 0.0.1 8 | * The {@link Action} store. 9 | */ 10 | export class ActionStore extends Store { 11 | 12 | /** 13 | * @since 0.0.1 14 | * @param client The client this Store was created with 15 | */ 16 | public constructor(client: Client) { 17 | super(client, 'actions', Action as PieceConstructor); 18 | } 19 | 20 | /** 21 | * Clears the actions from the store and removes the listeners. 22 | * @since 0.0.1 23 | */ 24 | public clear(): void { 25 | for (const event of this.values()) this.remove(event); 26 | } 27 | 28 | /** 29 | * Removes an action from the store. 30 | * @since 0.0.1 31 | * @param name An action object or a string representing the action name. 32 | * @returns Whether or not the removal was successful. 33 | */ 34 | public remove(name: Action | string): boolean { 35 | const event = this.resolve(name); 36 | if (!event) return false; 37 | // eslint-disable-next-line dot-notation 38 | event['_unlisten'](); 39 | return super.remove(event); 40 | } 41 | 42 | /** 43 | * Adds and sets up an action in our store. 44 | * @since 0.0.1 45 | * @param piece The event piece we are setting up 46 | */ 47 | public add(piece: Action): Action | null { 48 | const event = super.add(piece); 49 | if (!event) return null; 50 | // eslint-disable-next-line dot-notation 51 | event['_listen'](); 52 | return event; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/pieces/Event.ts: -------------------------------------------------------------------------------- 1 | import { Piece, PieceOptions } from './base/Piece'; 2 | import { Client, ClientEvents } from '../client/Client'; 3 | 4 | import type { EventEmitter } from 'events'; 5 | import type { EventStore } from './EventStore'; 6 | 7 | /** 8 | * The common class for all events. 9 | */ 10 | export abstract class Event extends Piece { 11 | 12 | /** 13 | * If this event should only be run once and then unloaded 14 | * @since 0.0.1 15 | */ 16 | public readonly once: boolean; 17 | 18 | /** 19 | * The emitter this event is for 20 | * @since 0.0.1 21 | */ 22 | public readonly emitter: EventEmitter; 23 | 24 | /** 25 | * The event to listen for 26 | * @since 0.0.1 27 | */ 28 | public readonly event: string; 29 | 30 | /** 31 | * Stored bound on method, so it can be properly unlistened to later 32 | * @since 0.0.1 33 | */ 34 | readonly #listener: Event['run']; 35 | 36 | /** 37 | * @since 0.0.1 38 | * @param store The store this piece is for 39 | * @param directory The base directory to the pieces folder 40 | * @param file The path from the pieces folder to the piece file 41 | * @param options The options for this piece 42 | */ 43 | public constructor(store: EventStore, directory: string, file: readonly string[], options: EventOptions = {}) { 44 | super(store, directory, file, options); 45 | this.once = options.once ?? false; 46 | this.emitter = (typeof options.emitter === 'string' ? this.client[options.emitter] as EventEmitter : options.emitter) ?? this.client; 47 | this.event = options.event ?? this.name; 48 | this.#listener = this.once ? this._runOnce.bind(this) : this._run.bind(this); 49 | } 50 | 51 | public abstract run(...args: readonly unknown[]): unknown; 52 | 53 | /** 54 | * Disables this Event 55 | * @since 0.0.1 56 | * @chainable 57 | */ 58 | public disable(): this { 59 | this._unlisten(); 60 | return super.disable(); 61 | } 62 | 63 | /** 64 | * Enables this Event 65 | * @since 0.0.1 66 | * @chainable 67 | */ 68 | public enable(): this { 69 | this._listen(); 70 | return super.enable(); 71 | } 72 | 73 | /** 74 | * A wrapper for the run method, to easily disable/enable events 75 | * @since 0.0.1 76 | * @param param The event parameters emitted 77 | */ 78 | private async _run(...args: Parameters): Promise { 79 | try { 80 | await this.run(...args); 81 | } catch (err) { 82 | this.client.emit(ClientEvents.EventError, this, args, err); 83 | } 84 | } 85 | 86 | /** 87 | * A wrapper for the _run method for once handling 88 | * @since 0.0.1 89 | * @param param The event parameters emitted 90 | */ 91 | private async _runOnce(...args: Parameters): Promise { 92 | await this._run(...args); 93 | // eslint-disable-next-line dot-notation 94 | this.store['_onceEvents'].add(this.file[this.file.length - 1]); 95 | this.unload(); 96 | } 97 | 98 | /** 99 | * Attaches the proper listener to the emitter 100 | * @since 0.0.1 101 | */ 102 | private _listen(): void { 103 | this.emitter[this.once ? 'once' : 'on'](this.event, this.#listener); 104 | } 105 | 106 | /** 107 | * Removes the listener from the emitter 108 | * @since 0.0.1 109 | */ 110 | private _unlisten(): void { 111 | this.emitter.removeListener(this.event, this.#listener); 112 | } 113 | 114 | /** 115 | * Defines the JSON.stringify behavior of this event. 116 | */ 117 | public toJSON(): Record { 118 | return { 119 | ...super.toJSON(), 120 | once: this.once, 121 | event: this.event, 122 | emitter: this.emitter.constructor.name 123 | }; 124 | } 125 | 126 | } 127 | 128 | export interface Event { 129 | store: EventStore; 130 | } 131 | 132 | /** 133 | * The piece options for all {@link Event} instances. 134 | */ 135 | export interface EventOptions extends PieceOptions { 136 | /** 137 | * Whether or not this event should only be run once and then unloaded 138 | */ 139 | once?: boolean; 140 | 141 | /** 142 | * The emitter this event should be for (string indicates a client property). 143 | */ 144 | emitter?: EventEmitter | keyof Client; 145 | 146 | /** 147 | * The event that should be listened to. 148 | */ 149 | event?: string; 150 | } 151 | -------------------------------------------------------------------------------- /src/lib/pieces/EventStore.ts: -------------------------------------------------------------------------------- 1 | import { Store, PieceConstructor } from './base/Store'; 2 | import { Event } from './Event'; 3 | 4 | import type { Client } from '../client/Client'; 5 | 6 | /** 7 | * @since 0.0.1 8 | * The {@link Event} store. 9 | */ 10 | export class EventStore extends Store { 11 | 12 | /** 13 | * Once events that have already run (so once means once). 14 | * @since 0.0.1 15 | */ 16 | private readonly _onceEvents = new Set(); 17 | 18 | /** 19 | * @since 0.0.1 20 | * @param client The client this Store was created with 21 | */ 22 | public constructor(client: Client) { 23 | super(client, 'events', Event as PieceConstructor); 24 | } 25 | 26 | /** 27 | * Loads a piece into Klasa so it can be saved in this store. 28 | * @since 0.0.1 29 | * @param file A string or array of strings showing where the file is located. 30 | * @param core If the file is located in the core directory or not 31 | */ 32 | public load(directory: string, file: readonly string[]): Promise { 33 | if (this._onceEvents.has(file[file.length - 1])) return Promise.resolve(null); 34 | return super.load(directory, file); 35 | } 36 | 37 | /** 38 | * Clears the events from the store and removes the listeners. 39 | * @since 0.0.1 40 | */ 41 | public clear(): void { 42 | for (const event of this.values()) this.remove(event); 43 | } 44 | 45 | /** 46 | * Removes an event from the store. 47 | * @since 0.0.1 48 | * @param name An event object or a string representing the event name. 49 | * @returns Whether or not the removal was successful. 50 | */ 51 | public remove(name: Event | string): boolean { 52 | const event = this.resolve(name); 53 | if (!event) return false; 54 | // eslint-disable-next-line dot-notation 55 | event['_unlisten'](); 56 | return super.remove(event); 57 | } 58 | 59 | /** 60 | * Adds and sets up an event in our store. 61 | * @since 0.0.1 62 | * @param piece The event piece we are setting up 63 | */ 64 | public add(piece: Event): Event | null { 65 | const event = super.add(piece); 66 | if (!event) return null; 67 | // eslint-disable-next-line dot-notation 68 | event['_listen'](); 69 | return event; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/pieces/base/AliasPiece.ts: -------------------------------------------------------------------------------- 1 | import { Piece, PieceOptions } from './Piece'; 2 | 3 | import type { Store } from './Store'; 4 | 5 | /** 6 | * The common class for all pieces with aliases. 7 | */ 8 | export class AliasPiece extends Piece { 9 | 10 | /** 11 | * The aliases for this piece. 12 | * @since 0.0.1 13 | */ 14 | public aliases: string[]; 15 | 16 | /** 17 | * @since 0.0.1 18 | * @param store The store this piece is for 19 | * @param directory The base directory to the pieces folder 20 | * @param file The path from the pieces folder to the piece file 21 | * @param options The options for this piece 22 | */ 23 | public constructor(store: Store, directory: string, file: readonly string[], options: AliasPieceOptions = {}) { 24 | super(store, directory, file, options); 25 | this.aliases = options.aliases?.slice() ?? []; 26 | } 27 | 28 | /** 29 | * Defines the JSON.stringify behavior of this argument. 30 | * @since 0.0.1 31 | */ 32 | public toJSON(): Record { 33 | return { 34 | ...super.toJSON(), 35 | aliases: this.aliases.slice() 36 | }; 37 | } 38 | 39 | } 40 | 41 | /** 42 | * The base piece options for all {@link AliasPiece} instances. 43 | */ 44 | export interface AliasPieceOptions extends PieceOptions { 45 | /** 46 | * The aliases for this piece. 47 | */ 48 | aliases?: string[]; 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/pieces/base/AliasStore.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@klasa/cache'; 2 | import { Store } from './Store'; 3 | 4 | import type { AliasPiece } from './AliasPiece'; 5 | 6 | /** 7 | * @since 0.0.1 8 | * The common base for all alias stores. 9 | */ 10 | export class AliasStore extends Store { 11 | 12 | /** 13 | * The different aliases that represent the arguments in this store. 14 | * @since 0.0.1 15 | */ 16 | public readonly aliases = new Cache(); 17 | 18 | /** 19 | * Returns an AliasPiece in the store if it exists by its name or by an alias. 20 | * @since 0.0.1 21 | * @param name A argument or alias name 22 | */ 23 | public get(name: string): V | undefined { 24 | return super.get(name) || this.aliases.get(name); 25 | } 26 | 27 | /** 28 | * Returns a boolean if the AliasPiece or alias is found within the store. 29 | * @since 0.0.1 30 | * @param name A piece or alias name 31 | */ 32 | public has(name: string): boolean { 33 | return super.has(name) || this.aliases.has(name); 34 | } 35 | 36 | /** 37 | * Adds and sets up an AliasPiece in our store. 38 | * @since 0.0.1 39 | * @param piece The piece we are setting up 40 | */ 41 | public add(piece: V): V | null { 42 | const aliasPiece = super.add(piece); 43 | if (!aliasPiece) return null; 44 | for (const alias of aliasPiece.aliases) this.aliases.set(alias, aliasPiece); 45 | return aliasPiece; 46 | } 47 | 48 | /** 49 | * Removes an AliasPiece from the store. 50 | * @since 0.0.1 51 | * @param name An AliasPiece object or a string representing an AliasPiece or alias name 52 | * @returns Whether or not the removal was successful. 53 | */ 54 | public remove(name: V | string): boolean { 55 | const aliasPiece = this.resolve(name) as V | null; 56 | if (!aliasPiece) return false; 57 | for (const alias of aliasPiece.aliases) this.aliases.delete(alias); 58 | return super.remove(aliasPiece); 59 | } 60 | 61 | /** 62 | * Clears the AliasPieces and aliases from this store 63 | * @since 0.0.1 64 | */ 65 | public clear(): void { 66 | super.clear(); 67 | this.aliases.clear(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/pieces/base/Piece.ts: -------------------------------------------------------------------------------- 1 | import { join, basename, extname } from 'path'; 2 | import { Client, ClientEvents } from '../../client/Client'; 3 | 4 | import type { Store } from './Store'; 5 | import { mergeDefault } from '@klasa/utils'; 6 | 7 | /** 8 | * The common class for all pieces. 9 | */ 10 | export class Piece { 11 | 12 | /** 13 | * The client this Piece was created with. 14 | * @since 0.0.1 15 | */ 16 | public readonly client: Client; 17 | 18 | /** 19 | * The store this Piece is from. 20 | * @since 0.0.1 21 | */ 22 | public readonly store: Store; 23 | 24 | /** 25 | * The file location where this Piece is stored. 26 | * @since 0.0.1 27 | */ 28 | public readonly file: readonly string[]; 29 | 30 | /** 31 | * The base directory this Piece is stored in. 32 | * @since 0.0.1 33 | */ 34 | public readonly directory: string; 35 | 36 | /** 37 | * The name of the Piece. 38 | * @since 0.0.1 39 | */ 40 | public name: string; 41 | 42 | /** 43 | * Whether or not the Piece is enabled. 44 | * @since 0.0.1 45 | */ 46 | public enabled: boolean; 47 | 48 | /** 49 | * @since 0.0.1 50 | * @param store The store this piece is for 51 | * @param directory The base directory to the pieces folder 52 | * @param file The path from the pieces folder to the piece file 53 | * @param options The options for this piece 54 | */ 55 | public constructor(store: Store, directory: string, file: readonly string[], options: PieceOptions = {}) { 56 | const defaults = Reflect.get(store.client.options.pieces.defaults, store.name) as Required; 57 | if (defaults) options = mergeDefault(defaults, options); 58 | this.client = store.client; 59 | this.store = store as Store; 60 | this.directory = directory; 61 | this.file = file; 62 | this.name = options.name ?? basename(file[file.length - 1], extname(file[file.length - 1])); 63 | this.enabled = options.enabled ?? true; 64 | } 65 | 66 | /** 67 | * The type of piece this is 68 | * @since 0.0.1 69 | */ 70 | public get type(): string { 71 | return this.store.name.slice(0, -1); 72 | } 73 | 74 | /** 75 | * The absolute path to this piece 76 | * @since 0.0.1 77 | */ 78 | public get path(): string { 79 | return join(this.directory, ...this.file); 80 | } 81 | 82 | /** 83 | * Reloads this piece 84 | * @since 0.0.1 85 | * @returns The newly loaded piece 86 | */ 87 | public async reload(): Promise { 88 | const piece = await this.store.load(this.directory, this.file); 89 | if (piece) { 90 | await piece.init(); 91 | this.client.emit(ClientEvents.PieceReloaded, piece); 92 | } 93 | return piece; 94 | } 95 | 96 | /** 97 | * Unloads this piece 98 | * @since 0.0.1 99 | */ 100 | public unload(): boolean { 101 | this.client.emit(ClientEvents.PieceUnloaded, this); 102 | return this.store.remove(this); 103 | } 104 | 105 | /** 106 | * Disables this piece 107 | * @since 0.0.1 108 | * @chainable 109 | */ 110 | public disable(): this { 111 | this.client.emit(ClientEvents.PieceDisabled, this); 112 | this.enabled = false; 113 | return this; 114 | } 115 | 116 | /** 117 | * Enables this piece 118 | * @since 0.0.1 119 | * @chainable 120 | */ 121 | public enable(): this { 122 | this.client.emit(ClientEvents.PieceEnabled, this); 123 | this.enabled = true; 124 | return this; 125 | } 126 | 127 | /** 128 | * The init method to be optionally overwritten in actual pieces 129 | * @since 0.0.1 130 | */ 131 | public init(): unknown { 132 | // Optionally defined in extension Classes 133 | return null; 134 | } 135 | 136 | /** 137 | * Defines toString behavior for pieces 138 | * @since 0.0.1 139 | * @returns This piece name 140 | */ 141 | public toString(): string { 142 | return this.name; 143 | } 144 | 145 | /** 146 | * Defines the JSON.stringify behavior of this piece. 147 | */ 148 | public toJSON(): Record { 149 | return { 150 | directory: this.directory, 151 | file: this.file, 152 | path: this.path, 153 | name: this.name, 154 | type: this.type, 155 | enabled: this.enabled 156 | }; 157 | } 158 | 159 | } 160 | 161 | /** 162 | * The base piece options for all {@link Piece} instances. 163 | */ 164 | export interface PieceOptions { 165 | /** 166 | * The name of the piece. Defaults to the filename without extension. 167 | */ 168 | name?: string; 169 | 170 | /** 171 | * Whether or not this piece should be enabled. Defaults to true. 172 | */ 173 | enabled?: boolean; 174 | } 175 | -------------------------------------------------------------------------------- /src/lib/util/Constants.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const Package = require('../../../../package.json'); 3 | 4 | import { RestOptionsDefaults } from '@klasa/rest'; 5 | import { WSOptionsDefaults } from '@klasa/ws'; 6 | 7 | import type { ClientOptions } from '../client/Client'; 8 | import type { BaseClientOptions } from '../client/BaseClient'; 9 | 10 | export const { version } = Package; 11 | 12 | export const BaseClientOptionsDefaults: Required = { 13 | rest: RestOptionsDefaults 14 | }; 15 | 16 | export const ClientOptionsDefaults: Required = { 17 | ...BaseClientOptionsDefaults, 18 | ws: WSOptionsDefaults, 19 | pieces: { 20 | defaults: { 21 | events: { 22 | enabled: true, 23 | once: false 24 | } 25 | }, 26 | createFolders: false, 27 | disabledCoreTypes: [] 28 | }, 29 | cache: { 30 | enabled: true, 31 | limits: { 32 | bans: Infinity, 33 | channels: Infinity, 34 | dms: Infinity, 35 | emojis: Infinity, 36 | guilds: Infinity, 37 | integrations: Infinity, 38 | invites: Infinity, 39 | members: Infinity, 40 | messages: 100, 41 | overwrites: Infinity, 42 | presences: Infinity, 43 | reactions: Infinity, 44 | roles: Infinity, 45 | users: Infinity, 46 | voiceStates: Infinity 47 | }, 48 | messageLifetime: 0, 49 | messageSweepInterval: 0 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/util/ImageUtil.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { Readable } from 'stream'; 3 | import { pathExists } from 'fs-nextra'; 4 | import { promises as fsp } from 'fs'; 5 | import { MessageAttachment } from '../caching/structures/messages/MessageAttachment'; 6 | 7 | /** 8 | * @param buffer The buffer the sniff the magic numbers from. 9 | * @see https://en.wikipedia.org/wiki/GIF 10 | * @private 11 | */ 12 | function isGif(buffer: Buffer): boolean { 13 | // 0x47 0x49 0x46 0x38 0x39 0x61 14 | return buffer.length > 6 && 15 | buffer[0] === 0x47 && 16 | buffer[1] === 0x49 && 17 | buffer[2] === 0x46 && 18 | buffer[3] === 0x38 && 19 | buffer[4] === 0x39 && 20 | buffer[5] === 0x61; 21 | } 22 | 23 | /** 24 | * @param buffer The buffer the sniff the magic numbers from. 25 | * @see https://en.wikipedia.org/wiki/JPEG 26 | * @private 27 | */ 28 | function isJpeg(buffer: Buffer): boolean { 29 | // 0xFF 0xD8 0xFF 30 | return buffer.length > 3 && 31 | buffer[0] === 0xFF && 32 | buffer[1] === 0xD8 && 33 | buffer[2] === 0xFF; 34 | } 35 | 36 | /** 37 | * @param buffer The buffer the sniff the magic numbers from. 38 | * @see https://en.wikipedia.org/wiki/Portable_Network_Graphics 39 | * @private 40 | */ 41 | function isPng(buffer: Buffer): boolean { 42 | // 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A 43 | return buffer.length > 8 && 44 | buffer[0] === 0x89 && 45 | buffer[1] === 0x50 && 46 | buffer[2] === 0x4E && 47 | buffer[3] === 0x47 && 48 | buffer[4] === 0x0D && 49 | buffer[5] === 0x0A && 50 | buffer[6] === 0x1A && 51 | buffer[7] === 0x0A; 52 | } 53 | 54 | /** 55 | * @param buffer The buffer the sniff the magic numbers from. 56 | * @see https://en.wikipedia.org/wiki/WebP 57 | * @private 58 | */ 59 | function isWebp(buffer: Buffer): boolean { 60 | // 0x52 0x49 0x46 0x46 0x__ 0x__ 0x__ 0x__ 0x57 0x45 0x42 0x50 61 | return buffer.length > 12 && 62 | buffer[0] === 0x52 && 63 | buffer[1] === 0x49 && 64 | buffer[2] === 0x46 && 65 | buffer[3] === 0x46 && 66 | buffer[8] === 0x57 && 67 | buffer[9] === 0x45 && 68 | buffer[10] === 0x42 && 69 | buffer[11] === 0x50; 70 | } 71 | 72 | export const enum ImageTypes { 73 | GIF = 'image/gif', 74 | JPEG = 'image/jpeg', 75 | PNG = 'image/png', 76 | WEBP = 'image/webp' 77 | } 78 | 79 | export type ImageBufferResolvable = Readable | Buffer | MessageAttachment | string; 80 | 81 | /** 82 | * Determines whether or not a buffer corresponds to a GIF, JPG/JPEG, PNG, or WebP. 83 | * @since 0.0.1 84 | * @param buffer The buffer to sniff the magic numbers from. 85 | */ 86 | export function getImageType(buffer: Buffer): ImageTypes | null { 87 | if (isGif(buffer)) return ImageTypes.GIF; 88 | if (isJpeg(buffer)) return ImageTypes.JPEG; 89 | if (isPng(buffer)) return ImageTypes.PNG; 90 | if (isWebp(buffer)) return ImageTypes.WEBP; 91 | return null; 92 | } 93 | 94 | /** 95 | * Determines the image's file type based on its contents and provides a base 64 string. 96 | * @since 0.0.1 97 | * @param buffer The buffer to sniff and stringify into a Base 64 string. 98 | * @param fallback The default image type to fall back to. 99 | */ 100 | export function imageToBase64(buffer: Buffer, fallback: ImageTypes = ImageTypes.JPEG): string { 101 | return `data:${getImageType(buffer) ?? fallback};base64,${buffer.toString('base64')}`; 102 | } 103 | 104 | /** 105 | * Converts a stream, buffer, message attachment, url, or filepath to a buffer. 106 | * @since 0.0.1 107 | * @param data The data to resolve into a buffer. 108 | */ 109 | export async function resolveImageBuffer(data: ImageBufferResolvable): Promise { 110 | if (data instanceof Readable) { 111 | const buffers = []; 112 | for await (const buffer of data) buffers.push(buffer); 113 | return Buffer.concat(buffers); 114 | } 115 | if (Buffer.isBuffer(data)) return data; 116 | if (data instanceof MessageAttachment) return (await fetch(data.url)).buffer(); 117 | if (/^https?:\/\//.test(data)) return (await fetch(data)).buffer(); 118 | if (await pathExists(data)) return fsp.readFile(data); 119 | return Buffer.from(data); 120 | } 121 | 122 | /** 123 | * Determines the image's file type based on its contents and provides a base 64 string. 124 | * @since 0.0.1 125 | * @param data The data to resolve into a Base 64 string. 126 | * @param fallback The default image type to fall back to. 127 | */ 128 | export async function resolveImageToBase64(data: ImageBufferResolvable, fallback?: ImageTypes): Promise { 129 | const resolved = await resolveImageBuffer(data); 130 | return imageToBase64(resolved, fallback); 131 | } 132 | -------------------------------------------------------------------------------- /src/lib/util/Util.ts: -------------------------------------------------------------------------------- 1 | import type { Channel } from '../caching/structures/channels/Channel'; 2 | import type { DMChannel } from '../caching/structures/channels/DMChannel'; 3 | import type { TextChannel } from '../caching/structures/channels/TextChannel'; 4 | import type { NewsChannel } from '../caching/structures/channels/NewsChannel'; 5 | import type { CategoryChannel } from '../caching/structures/channels/CategoryChannel'; 6 | import type { VoiceChannel } from '../caching/structures/channels/VoiceChannel'; 7 | import type { StoreChannel } from '../caching/structures/channels/StoreChannel'; 8 | import type { GuildEmoji } from '../caching/structures/guilds/GuildEmoji'; 9 | import type { MessageReactionEmoji } from '../caching/structures/messages/reactions/MessageReactionEmoji'; 10 | 11 | export function snakeToCamel(input: string): string { 12 | const [first, ...parts] = input.split('_'); 13 | 14 | let output = first.toLowerCase(); 15 | for (const part of parts) { 16 | output += part[0].toUpperCase() + part.substr(1).toLowerCase(); 17 | } 18 | 19 | return output; 20 | } 21 | 22 | export type GuildBasedChannel = TextChannel | NewsChannel | VoiceChannel | CategoryChannel | StoreChannel; 23 | export type GuildTextBasedChannel = TextChannel | NewsChannel; 24 | export type TextBasedChannel = DMChannel | GuildTextBasedChannel; 25 | export type Channels = DMChannel | GuildBasedChannel; 26 | 27 | export function isTextBasedChannel(channel: Channel): channel is TextBasedChannel { 28 | return Reflect.has(channel, 'messages'); 29 | } 30 | 31 | export function isGuildTextBasedChannel(channel: Channel): channel is GuildTextBasedChannel { 32 | return isTextBasedChannel(channel) && isGuildChannel(channel); 33 | } 34 | 35 | export function isGuildChannel(channel: Channel): channel is GuildBasedChannel { 36 | return Reflect.has(channel, 'guild'); 37 | } 38 | 39 | // eslint-disable-next-line @typescript-eslint/ban-types 40 | export function isSet(value: V, key: K): value is V & Required> { 41 | return Reflect.has(value, key); 42 | } 43 | 44 | export type EmojiResolvable = string | MessageReactionEmoji | GuildEmoji; 45 | 46 | export function resolveEmoji(emoji: EmojiResolvable): string { 47 | if (typeof emoji === 'string') { 48 | // <:klasa:354702113147846666> -> :klasa:354702113147846666 49 | if (emoji.startsWith('<')) return emoji.slice(1, -1); 50 | 51 | // :klasa:354702113147846666 -> :klasa:354702113147846666 52 | // a:klasa:354702113147846666 -> a:klasa:354702113147846666 53 | if (emoji.startsWith(':') || emoji.startsWith('a:')) return emoji; 54 | 55 | // 🚀 -> %F0%9F%9A%80 56 | return encodeURIComponent(emoji); 57 | } 58 | 59 | // Safe-guard against https://github.com/discordapp/discord-api-docs/issues/974 60 | return emoji.id ? `${emoji.animated ? 'a' : ''}:${(emoji.name as string).replace(/~\d+/, '')}:${emoji.id}` : encodeURIComponent(emoji.name as string); 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/util/bitfields/Activity.ts: -------------------------------------------------------------------------------- 1 | import { BitField, BitFieldObject } from '@klasa/bitfield'; 2 | 3 | /* eslint-disable no-bitwise */ 4 | 5 | export const enum ActivityFlags { 6 | Instance = 'INSTANCE', 7 | Join = 'JOIN', 8 | Spectate = 'SPECTATE', 9 | JoinRequest = 'JOIN_REQUEST', 10 | Sync = 'SYNC', 11 | Play ='PLAY' 12 | } 13 | 14 | export type ActivityResolvable = ActivityFlags | number | BitFieldObject | (ActivityFlags | number | BitFieldObject)[]; 15 | 16 | /** 17 | * Handles Activity BitFields in Klasa-Core 18 | */ 19 | export class Activity extends BitField { 20 | 21 | /** 22 | * The Activity flags 23 | */ 24 | public static FLAGS = { 25 | [ActivityFlags.Instance]: 1 << 0, 26 | [ActivityFlags.Join]: 1 << 1, 27 | [ActivityFlags.Spectate]: 1 << 2, 28 | [ActivityFlags.JoinRequest]: 1 << 3, 29 | [ActivityFlags.Sync]: 1 << 4, 30 | [ActivityFlags.Play]: 1 << 5 31 | } as const; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/util/bitfields/MessageFlags.ts: -------------------------------------------------------------------------------- 1 | import { BitField, BitFieldObject } from '@klasa/bitfield'; 2 | 3 | /* eslint-disable no-bitwise */ 4 | 5 | export const enum MessageFlagsFlags { 6 | Crossposted = 'CROSSPOSTED', 7 | IsCrosspost = 'IS_CROSSPOST', 8 | SuppressEmbeds = 'SUPPRESS_EMBEDS', 9 | SourceMessageDeleted = 'SOURCE_MESSAGE_DELETED', 10 | Urgent = 'URGENT' 11 | } 12 | 13 | export type MessageFlagsResolvable = MessageFlagsFlags | number | BitFieldObject | (MessageFlagsFlags | number | BitFieldObject)[]; 14 | 15 | /** 16 | * Handles MessageFlags BitFields in Klasa-Core 17 | */ 18 | export class MessageFlags extends BitField { 19 | 20 | /** 21 | * The MessageFlags flags 22 | */ 23 | public static FLAGS = { 24 | [MessageFlagsFlags.Crossposted]: 1 << 0, 25 | [MessageFlagsFlags.IsCrosspost]: 1 << 1, 26 | [MessageFlagsFlags.SuppressEmbeds]: 1 << 2, 27 | [MessageFlagsFlags.SourceMessageDeleted]: 1 << 3, 28 | [MessageFlagsFlags.Urgent]: 1 << 4 29 | } as const; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/util/bitfields/Permissions.ts: -------------------------------------------------------------------------------- 1 | import { BitField, BitFieldObject } from '@klasa/bitfield'; 2 | 3 | /* eslint-disable no-bitwise */ 4 | 5 | export const enum PermissionsFlags { 6 | CreateInstantInvite = 'CREATE_INSTANT_INVITE', 7 | KickMembers = 'KICK_MEMBERS', 8 | BanMembers = 'BAN_MEMBERS', 9 | Administrator = 'ADMINISTRATOR', 10 | ManageChannels = 'MANAGE_CHANNELS', 11 | ManageGuild = 'MANAGE_GUILD', 12 | AddReactions = 'ADD_REACTIONS', 13 | ViewAuditLog = 'VIEW_AUDIT_LOG', 14 | PrioritySpeaker = 'PRIORITY_SPEAKER', 15 | Stream = 'STREAM', 16 | ViewChannel = 'VIEW_CHANNEL', 17 | SendMessages = 'SEND_MESSAGES', 18 | SendTTSMessages = 'SEND_TTS_MESSAGES', 19 | ManageMessages = 'MANAGE_MESSAGES', 20 | EmbedLinks = 'EMBED_LINKS', 21 | AttachFiles = 'ATTACH_FILES', 22 | ReadMessageHistory = 'READ_MESSAGE_HISTORY', 23 | MentionEveryone = 'MENTION_EVERYONE', 24 | UseExternalEmojis = 'USE_EXTERNAL_EMOJIS', 25 | ViewGuildInsights = 'VIEW_GUILD_INSIGHTS', 26 | 27 | Connect = 'CONNECT', 28 | Speak = 'SPEAK', 29 | MuteMembers = 'MUTE_MEMBERS', 30 | DeafenMembers = 'DEAFEN_MEMBERS', 31 | MoveMembers = 'MOVE_MEMBERS', 32 | UseVAD = 'USE_VAD', 33 | 34 | ChangeNickname = 'CHANGE_NICKNAME', 35 | ManageNicknames = 'MANAGE_NICKNAMES', 36 | ManageRoles = 'MANAGE_ROLES', 37 | ManageWebhooks = 'MANAGE_WEBHOOKS', 38 | ManageEmojis = 'MANAGE_EMOJIS' 39 | } 40 | 41 | export type PermissionsResolvable = PermissionsFlags | number | BitFieldObject | (PermissionsFlags | number | BitFieldObject)[]; 42 | 43 | /* eslint-disable no-bitwise */ 44 | 45 | /** 46 | * Handles Permission BitFields in Klasa-Core 47 | */ 48 | export class Permissions extends BitField { 49 | 50 | /** 51 | * The Permissions flags 52 | */ 53 | public static FLAGS = { 54 | [PermissionsFlags.CreateInstantInvite]: 1 << 0, 55 | [PermissionsFlags.KickMembers]: 1 << 1, 56 | [PermissionsFlags.BanMembers]: 1 << 2, 57 | [PermissionsFlags.Administrator]: 1 << 3, 58 | [PermissionsFlags.ManageChannels]: 1 << 4, 59 | [PermissionsFlags.ManageGuild]: 1 << 5, 60 | [PermissionsFlags.AddReactions]: 1 << 6, 61 | [PermissionsFlags.ViewAuditLog]: 1 << 7, 62 | [PermissionsFlags.PrioritySpeaker]: 1 << 8, 63 | [PermissionsFlags.Stream]: 1 << 9, 64 | [PermissionsFlags.ViewChannel]: 1 << 10, 65 | [PermissionsFlags.SendMessages]: 1 << 11, 66 | [PermissionsFlags.SendTTSMessages]: 1 << 12, 67 | [PermissionsFlags.ManageMessages]: 1 << 13, 68 | [PermissionsFlags.EmbedLinks]: 1 << 14, 69 | [PermissionsFlags.AttachFiles]: 1 << 15, 70 | [PermissionsFlags.ReadMessageHistory]: 1 << 16, 71 | [PermissionsFlags.MentionEveryone]: 1 << 17, 72 | [PermissionsFlags.UseExternalEmojis]: 1 << 18, 73 | [PermissionsFlags.ViewGuildInsights]: 1 << 19, 74 | 75 | [PermissionsFlags.Connect]: 1 << 20, 76 | [PermissionsFlags.Speak]: 1 << 21, 77 | [PermissionsFlags.MuteMembers]: 1 << 22, 78 | [PermissionsFlags.DeafenMembers]: 1 << 23, 79 | [PermissionsFlags.MoveMembers]: 1 << 24, 80 | [PermissionsFlags.UseVAD]: 1 << 25, 81 | 82 | [PermissionsFlags.ChangeNickname]: 1 << 26, 83 | [PermissionsFlags.ManageNicknames]: 1 << 27, 84 | [PermissionsFlags.ManageRoles]: 1 << 28, 85 | [PermissionsFlags.ManageWebhooks]: 1 << 29, 86 | [PermissionsFlags.ManageEmojis]: 1 << 30 87 | } as const; 88 | 89 | /** 90 | * The default permissions granted 91 | */ 92 | public static DEFAULT = 104324673; 93 | 94 | /** 95 | * Permissions that cannot be influenced by channel overwrites, even if explicitly set. 96 | */ 97 | public static GUILD_SCOPE_PERMISSIONS = 1275592878; 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/lib/util/bitfields/Speaking.ts: -------------------------------------------------------------------------------- 1 | import { BitField, BitFieldObject } from '@klasa/bitfield'; 2 | 3 | /* eslint-disable no-bitwise */ 4 | 5 | export const enum SpeakingFlags { 6 | Speaking = 'SPEAKING', 7 | Soundshare = 'SOUNDSHARE' 8 | } 9 | 10 | export type SpeakingResolvable = SpeakingFlags | number | BitFieldObject | (SpeakingFlags | number | BitFieldObject)[]; 11 | 12 | /** 13 | * Handles Speaking BitFields in Klasa-Core 14 | */ 15 | export class Speaking extends BitField { 16 | 17 | /** 18 | * The Speaking flags 19 | */ 20 | public static FLAGS = { 21 | [SpeakingFlags.Speaking]: 1 << 0, 22 | [SpeakingFlags.Soundshare]: 1 << 1 23 | } as const; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/util/bitfields/UserFlags.ts: -------------------------------------------------------------------------------- 1 | import { BitField, BitFieldObject } from '@klasa/bitfield'; 2 | 3 | /* eslint-disable no-bitwise */ 4 | 5 | export const enum UserFlagsFlags { 6 | DiscordEmployee = 'DISCORD_EMPLOYEE', 7 | DiscordPartner = 'DISCORD_PARTNER', 8 | HypesquadEvents = 'HYPESQUAD_EVENTS', 9 | BugHunterLevel1 = 'BUG_HUNTER_LEVEL_1', 10 | HouseBravery = 'HOUSE_BRAVERY', 11 | HouseBrilliance = 'HOUSE_BRILLIANCE', 12 | HouseBalance = 'HOUSE_BALANCE', 13 | EarlySupporter = 'EARLY_SUPPORTER', 14 | TeamUser = 'TEAM_USER', 15 | System = 'SYSTEM', 16 | BugHunterLevel2 = 'BUG_HUNTER_LEVEL_2' 17 | } 18 | 19 | export type UserFlagsResolvable = UserFlagsFlags | number | BitFieldObject | (UserFlagsFlags | number | BitFieldObject)[]; 20 | 21 | /** 22 | * Handles UserFlags BitFields in Klasa-Core 23 | */ 24 | export class UserFlags extends BitField { 25 | 26 | /** 27 | * The UserFlags flags 28 | */ 29 | public static FLAGS = { 30 | [UserFlagsFlags.DiscordEmployee]: 1 << 0, 31 | [UserFlagsFlags.DiscordPartner]: 1 << 1, 32 | [UserFlagsFlags.HypesquadEvents]: 1 << 2, 33 | [UserFlagsFlags.BugHunterLevel1]: 1 << 3, 34 | [UserFlagsFlags.HouseBravery]: 1 << 6, 35 | [UserFlagsFlags.HouseBrilliance]: 1 << 7, 36 | [UserFlagsFlags.HouseBalance]: 1 << 8, 37 | [UserFlagsFlags.EarlySupporter]: 1 << 9, 38 | [UserFlagsFlags.TeamUser]: 1 << 10, 39 | [UserFlagsFlags.System]: 1 << 12, 40 | [UserFlagsFlags.BugHunterLevel2]: 1 << 14 41 | } as const; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/util/collectors/MessageCollector.ts: -------------------------------------------------------------------------------- 1 | import { MessageIterator } from '../iterators/MessageIterator'; 2 | import { StructureCollector } from './base/StructureCollector'; 3 | 4 | import type { Cache } from '@klasa/cache'; 5 | import type { DMChannel } from '../../caching/structures/channels/DMChannel'; 6 | import type { GuildTextChannel } from '../../caching/structures/channels/GuildTextChannel'; 7 | import type { Message } from '../../caching/structures/messages/Message'; 8 | 9 | /** 10 | * Options for a MessageCollector. 11 | * @since 0.0.1 12 | */ 13 | export interface MessageCollectorOptions { 14 | /** 15 | * The amount of messages to collect before ending the collector. 16 | * @since 0.0.1 17 | */ 18 | limit?: number; 19 | /** 20 | * The time in ms that a MessageCollector will go before idling out. 21 | * @since 0.0.1 22 | */ 23 | idle?: number; 24 | /** 25 | * The filter used to filter out specific messages. 26 | * @since 0.0.1 27 | */ 28 | filter?: (message: [Message], collected: Cache) => boolean; 29 | } 30 | 31 | /** 32 | * The MessageCollector class responsible for collecting a set of messages. 33 | * @since 0.0.1 34 | */ 35 | export class MessageCollector extends StructureCollector { 36 | 37 | /** 38 | * Construct's a new MessageCollector. 39 | * @since 0.0.1 40 | * @param channel The channel to listen for messages. 41 | * @param options Any additional options to pass. 42 | */ 43 | public constructor(channel: GuildTextChannel | DMChannel, options: MessageCollectorOptions) { 44 | if (!options.limit && !options.idle) throw new Error('Collectors need either a limit or idle, or they will collect forever.'); 45 | const { limit, idle, filter = (): boolean => true } = options; 46 | 47 | super(new MessageIterator(channel, { 48 | limit, 49 | idle, 50 | filter: (message): boolean => filter(message, this.collected) 51 | })); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/util/collectors/ReactionCollector.ts: -------------------------------------------------------------------------------- 1 | import { StructureCollector } from './base/StructureCollector'; 2 | import { ReactionIterator } from '../iterators/ReactionIterator'; 3 | 4 | import type { Cache } from '@klasa/cache'; 5 | import type { Message } from '../../caching/structures/messages/Message'; 6 | import type { MessageReaction } from '../../caching/structures/messages/reactions/MessageReaction'; 7 | import type { User } from '../../../lib/caching/structures/User'; 8 | 9 | /** 10 | * Options for a ReactionCollector. 11 | * @since 0.0.1 12 | */ 13 | export interface ReactionCollectorOptions { 14 | /** 15 | * The amount of reactions to collect before ending the collector. 16 | * @since 0.0.1 17 | */ 18 | limit?: number; 19 | /** 20 | * The time in ms that a ReactionCollector will go before idling out. 21 | * @since 0.0.1 22 | */ 23 | idle?: number; 24 | /** 25 | * The filter used to filter out specific reactions. 26 | * @since 0.0.1 27 | */ 28 | filter?: ([reaction, user]: [MessageReaction, User], collected: Cache) => boolean; 29 | } 30 | 31 | /** 32 | * The ReactionCollector class responsible for collecting a set of reactions. 33 | * @since 0.0.1 34 | */ 35 | export class ReactionCollector extends StructureCollector { 36 | 37 | /** 38 | * Construct's a new ReactionCollector. 39 | * @since 0.0.1 40 | * @param message The message to listen for reactions. 41 | * @param options Any additional options to pass. 42 | */ 43 | public constructor(message: Message, options: ReactionCollectorOptions) { 44 | if (!options.limit && !options.idle) throw new Error('Collectors need either a limit or idle, or they will collect forever.'); 45 | const { limit, idle, filter = (): boolean => true } = options; 46 | 47 | super(new ReactionIterator(message, { 48 | limit, 49 | idle, 50 | filter: ([reaction, user]): boolean => reaction.message === message && filter([reaction, user], this.collected) 51 | })); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/util/collectors/base/StructureCollector.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@klasa/cache'; 2 | 3 | import type { EventIterator } from '@klasa/event-iterator'; 4 | import type { Structure } from '../../../caching/structures/base/Structure'; 5 | 6 | /** 7 | * The base structure collector for asynchronously collecting values. 8 | * @since 0.0.1 9 | */ 10 | export class StructureCollector> { 11 | 12 | /** 13 | * The collected values. 14 | * @since 0.0.1 15 | */ 16 | protected collected = new Cache(); 17 | 18 | /** 19 | * The event iterator that's yielding values. 20 | * @since 0.0.1 21 | */ 22 | #iterator: I; 23 | 24 | /** 25 | * @since 0.0.1 26 | * @param iterator The EventIterator that is yielding values. 27 | */ 28 | public constructor(iterator: I) { 29 | this.#iterator = iterator; 30 | } 31 | 32 | /** 33 | * Collect's the values into the Collector's cache. 34 | * @since 0.0.1 35 | */ 36 | public async collect(): Promise> { 37 | for await (const [struct] of this.#iterator) this.collected.set(struct.id, struct); 38 | return this.collected; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/util/iterators/MessageIterator.ts: -------------------------------------------------------------------------------- 1 | import { EventIterator, EventIteratorOptions } from '@klasa/event-iterator'; 2 | import { ClientEvents } from '../../client/Client'; 3 | 4 | import type { DMChannel } from '../../caching/structures/channels/DMChannel'; 5 | import type { GuildTextChannel } from '../../caching/structures/channels/GuildTextChannel'; 6 | import type { Message } from '../../caching/structures/messages/Message'; 7 | 8 | export type MessageIteratorOptions = EventIteratorOptions<[Message]>; 9 | 10 | /** 11 | * An asynchronous iterator responsible for iterating over messages. 12 | * @since 0.0.1 13 | */ 14 | export class MessageIterator extends EventIterator<[Message]> { 15 | 16 | /** 17 | * Construct's a new MessageIterator. 18 | * @since 0.0.1 19 | * @param channel The channel to listen for messages. 20 | * @param options Any additional options to pass. 21 | */ 22 | public constructor(channel: GuildTextChannel | DMChannel, options: MessageIteratorOptions = {}) { 23 | const { limit, idle, filter = (): boolean => true } = options; 24 | 25 | super(channel.client, ClientEvents.MessageCreate, { 26 | limit, 27 | idle, 28 | filter: ([message]): boolean => message.channel === channel && filter([message]) 29 | }); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/util/iterators/ReactionIterator.ts: -------------------------------------------------------------------------------- 1 | import { EventIterator, EventIteratorOptions } from '@klasa/event-iterator'; 2 | import { ClientEvents } from '../../client/Client'; 3 | 4 | import type { Message } from '../../caching/structures/messages/Message'; 5 | import type { MessageReaction } from '../../caching/structures/messages/reactions/MessageReaction'; 6 | import type { User } from '../../../lib/caching/structures/User'; 7 | 8 | export type ReactionIteratorOptions = EventIteratorOptions<[MessageReaction, User]>; 9 | 10 | /** 11 | * An asynchronous iterator responsible for iterating over reactions. 12 | * @since 0.0.1 13 | */ 14 | export class ReactionIterator extends EventIterator<[MessageReaction, User]> { 15 | 16 | /** 17 | * Construct's a new ReactionIterator. 18 | * @since 0.0.1 19 | * @param channel The message to listen for reactions. 20 | * @param options Any additional options to pass. 21 | */ 22 | public constructor(message: Message, options: ReactionIteratorOptions = {}) { 23 | const { limit, idle, filter = (): boolean => true } = options; 24 | 25 | super(message.client, ClientEvents.MessageReactionAdd, { 26 | limit, 27 | idle, 28 | filter: ([reaction, user]): boolean => reaction.message === message && filter([reaction, user]) 29 | }); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /test/Application.ts: -------------------------------------------------------------------------------- 1 | import ava from 'ava'; 2 | import nock = require('nock'); 3 | import { RestOptionsDefaults, Routes } from '@klasa/rest'; 4 | import { Client, Application } from '../src'; 5 | 6 | import type { APIUserData, APIOauthData, APIGuildData } from '@klasa/dapi-types'; 7 | 8 | /* eslint-disable camelcase */ 9 | 10 | const rawGuild: APIGuildData = { 11 | id: '551164951497899949', 12 | name: 'Some Guild Headquarters', 13 | icon: 'eed97f542f2a60e9cafcb469abfef5f1', 14 | description: null, 15 | splash: null, 16 | discovery_splash: null, 17 | features: [], 18 | emojis: [], 19 | banner: null, 20 | owner_id: '167383252271628288', 21 | application_id: null, 22 | region: 'us-west', 23 | afk_channel_id: null, 24 | afk_timeout: 300, 25 | system_channel_id: null, 26 | widget_enabled: true, 27 | verification_level: 4, 28 | roles: [], 29 | default_message_notifications: 1, 30 | mfa_level: 1, 31 | explicit_content_filter: 1, 32 | max_presences: null, 33 | max_members: 250000, 34 | max_video_channel_users: 25, 35 | vanity_url_code: null, 36 | premium_tier: 1, 37 | premium_subscription_count: 2, 38 | system_channel_flags: 0, 39 | preferred_locale: 'en-US', 40 | rules_channel_id: null, 41 | public_updates_channel_id: null, 42 | embed_enabled: true 43 | }; 44 | 45 | const rawOwner: APIUserData = { 46 | id: '167383252271628288', 47 | username: 'Owner', 48 | avatar: '6b26b2972fe980d745dcced464dc7cff', 49 | discriminator: '0001', 50 | public_flags: 131840, 51 | flags: 131840 52 | }; 53 | 54 | const rawApplication: APIOauthData = { 55 | id: '228831628164566615', 56 | name: 'Klasa', 57 | icon: '5decfa5644c11081120c2f1b032d7c67', 58 | description: '', 59 | rpc_origins: ['http://localhost:3344'], 60 | summary: '', 61 | bot_public: true, 62 | bot_require_code_grant: false, 63 | verify_key: 'You-Do-Not-Want-This', 64 | owner: rawOwner, 65 | guild_id: '551164951497899949', 66 | primary_sku_id: '440053840386788838', 67 | slug: 'Some Slug!', 68 | cover_image: '5higje5644g11081120g2j1f032h7g67' 69 | }; 70 | 71 | /* eslint-enable camelcase */ 72 | 73 | nock(`${RestOptionsDefaults.api}/v${RestOptionsDefaults.version}`) 74 | .get(Routes.oauthApplication()) 75 | .times(Infinity) 76 | .reply(204, rawApplication); 77 | 78 | const client = new Client(); 79 | 80 | client.token = 'Not-A-Real-Token'; 81 | 82 | ava('fetch application', async (test): Promise => { 83 | const application = await Application.fetch(client); 84 | 85 | test.is(application.client, client); 86 | test.is(application.id, rawApplication.id); 87 | test.is(application.name, rawApplication.name); 88 | test.is(application.icon, rawApplication.icon); 89 | test.is(application.description, rawApplication.description); 90 | test.deepEqual(application.rpcOrigins, rawApplication.rpc_origins); 91 | test.is(application.botPublic, rawApplication.bot_public); 92 | test.is(application.botRequireCodeGrant, rawApplication.bot_require_code_grant); 93 | test.is(application.summary, rawApplication.summary); 94 | test.is(application.verifyKey, rawApplication.verify_key); 95 | test.is(application.guildID, rawApplication.guild_id); 96 | test.is(application.primarySkuID, rawApplication.primary_sku_id); 97 | test.is(application.slug, rawApplication.slug); 98 | test.is(application.coverImage, rawApplication.cover_image); 99 | test.is(application.team, null); 100 | 101 | test.not(application.owner, null); 102 | const { owner } = application; 103 | test.is(owner.id, rawOwner.id); 104 | test.is(owner.username, rawOwner.username); 105 | test.is(owner.avatar, rawOwner.avatar); 106 | test.is(owner.discriminator, rawOwner.discriminator); 107 | test.is(owner.publicFlags, rawOwner.public_flags); 108 | test.is(owner.flags, rawOwner.flags); 109 | 110 | // Test guild availability 111 | test.is(application.guild, null); 112 | // eslint-disable-next-line dot-notation 113 | const guild = client.guilds['_add'](rawGuild); 114 | test.is(application.guild, guild); 115 | }); 116 | -------------------------------------------------------------------------------- /test/BaseClient.ts: -------------------------------------------------------------------------------- 1 | import ava from 'ava'; 2 | import { TimerManager } from '@klasa/timer-manager'; 3 | import { BaseClient } from '../src'; 4 | 5 | const client = new BaseClient(); 6 | 7 | TimerManager.setInterval(() => { 8 | // foo 9 | }, 6000); 10 | 11 | TimerManager.setTimeout(() => { 12 | // Bar 13 | }, 600000); 14 | 15 | ava('destroy', async (test): Promise => { 16 | test.plan(4); 17 | 18 | // eslint-disable-next-line dot-notation 19 | test.is(TimerManager['_timeouts'].size, 1); 20 | // Second one is the RestManager sweep interval 21 | // eslint-disable-next-line dot-notation 22 | test.is(TimerManager['_intervals'].size, 2); 23 | await client.destroy(); 24 | // eslint-disable-next-line dot-notation 25 | test.is(TimerManager['_timeouts'].size, 0); 26 | // eslint-disable-next-line dot-notation 27 | test.is(TimerManager['_intervals'].size, 0); 28 | }); 29 | -------------------------------------------------------------------------------- /test/Team.ts: -------------------------------------------------------------------------------- 1 | import ava from 'ava'; 2 | import nock = require('nock'); 3 | import { RestOptionsDefaults, Routes } from '@klasa/rest'; 4 | import { Client, Application, Team, TeamMember } from '../src'; 5 | 6 | import type { APIUserData, APIOauthData, APITeamData, APITeamMember } from '@klasa/dapi-types'; 7 | 8 | /* eslint-disable camelcase */ 9 | 10 | const rawOwner: APIUserData = { 11 | id: '339942739275677726', 12 | username: 'team339942739275677726', 13 | avatar: null, 14 | discriminator: '0000', 15 | public_flags: 1024, 16 | flags: 1024 17 | }; 18 | 19 | const rawTeamMemberUser: APIUserData = { 20 | id: '167383252271628288', 21 | username: 'Owner', 22 | avatar: '6b26b2972fe980d745dcced464dc7cff', 23 | discriminator: '0001', 24 | public_flags: 131840 25 | }; 26 | 27 | const rawTeamMember: APITeamMember = { 28 | user: rawTeamMemberUser, 29 | team_id: '339942739275677726', 30 | membership_state: 2, 31 | permissions: ['*'] 32 | }; 33 | 34 | const rawTeam: APITeamData = { 35 | id: '339942739275677726', 36 | icon: '5decfa5644c11081120c2f1b032d7c67', 37 | owner_user_id: '167383252271628288', 38 | members: [rawTeamMember] 39 | }; 40 | 41 | const rawApplication: APIOauthData = { 42 | id: '228831628164566615', 43 | name: 'Klasa', 44 | icon: '5decfa5644c11081120c2f1b032d7c67', 45 | description: '', 46 | summary: '', 47 | bot_public: true, 48 | bot_require_code_grant: false, 49 | verify_key: 'You-Do-Not-Want-This', 50 | owner: rawOwner, 51 | team: rawTeam 52 | }; 53 | 54 | /* eslint-enable camelcase */ 55 | 56 | nock(`${RestOptionsDefaults.api}/v${RestOptionsDefaults.version}`) 57 | .get(Routes.oauthApplication()) 58 | .times(Infinity) 59 | .reply(204, rawApplication); 60 | 61 | const client = new Client(); 62 | 63 | client.token = 'Not-A-Real-Token'; 64 | 65 | ava('fetch application', async (test): Promise => { 66 | const application = await Application.fetch(client); 67 | 68 | test.is(application.client, client); 69 | test.is(application.id, rawApplication.id); 70 | test.is(application.name, rawApplication.name); 71 | test.is(application.icon, rawApplication.icon); 72 | test.is(application.description, rawApplication.description); 73 | test.deepEqual(application.rpcOrigins, []); 74 | test.is(application.botPublic, rawApplication.bot_public); 75 | test.is(application.botRequireCodeGrant, rawApplication.bot_require_code_grant); 76 | test.is(application.summary, rawApplication.summary); 77 | test.is(application.verifyKey, rawApplication.verify_key); 78 | test.is(application.guildID, null); 79 | test.is(application.guild, null); 80 | test.is(application.primarySkuID, null); 81 | test.is(application.slug, null); 82 | test.is(application.coverImage, null); 83 | 84 | test.not(application.team, null); 85 | const team = application.team as Team; 86 | test.is(team.client, client); 87 | test.is(team.id, rawTeam.id); 88 | test.is(team.icon, rawTeam.icon); 89 | test.is(team.ownerID, rawTeam.owner_user_id); 90 | test.is(team.members.size, 1); 91 | 92 | const teamMember = team.members.get(rawTeamMember.user.id) as TeamMember; 93 | test.truthy(teamMember); 94 | test.is(team.owner, teamMember); 95 | test.is(teamMember.client, client); 96 | test.is(teamMember.membershipState, rawTeamMember.membership_state); 97 | test.is(teamMember.id, rawTeamMemberUser.id); 98 | test.is(teamMember.toString(), `<@${rawTeamMemberUser.id}>`); 99 | test.deepEqual(teamMember.permissions, rawTeamMember.permissions); 100 | 101 | const teamMemberUser = teamMember.user; 102 | test.is(teamMemberUser.id, rawTeamMemberUser.id); 103 | test.is(teamMemberUser.username, rawTeamMemberUser.username); 104 | test.is(teamMemberUser.avatar, rawTeamMemberUser.avatar); 105 | test.is(teamMemberUser.discriminator, rawTeamMemberUser.discriminator); 106 | test.is(teamMemberUser.publicFlags, rawTeamMemberUser.public_flags); 107 | }); 108 | -------------------------------------------------------------------------------- /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", "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/core": ["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 | } --------------------------------------------------------------------------------