├── .all-contributorsrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── SECURITY.md ├── husky │ ├── .gitignore │ ├── commit-msg │ └── pre-commit ├── problemMatchers │ ├── eslint.json │ └── tsc.json ├── renovate.json └── workflows │ ├── codeql-analysis.yml │ ├── continuous-delivery.yml │ ├── continuous-integration.yml │ └── labelsync.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ └── plugin-typescript.cjs └── releases │ └── yarn-3.2.0.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE.md ├── OTHER_LICENSES.md ├── README.md ├── jest.config.ts ├── package.json ├── scripts └── clean.mjs ├── src ├── Cluster.ts ├── ClusterNode.ts ├── Node.ts ├── base │ ├── BaseCluster.ts │ └── BaseNode.ts ├── core │ ├── Connection.ts │ ├── Http.ts │ ├── Player.ts │ ├── PlayerStore.ts │ └── RoutePlanner.ts ├── index.ts ├── tsconfig.json └── types │ ├── IncomingPayloads.ts │ └── OutgoingPayloads.ts ├── tests ├── Cluster.test.ts.temp ├── index.test.ts ├── index.test.ts.temp ├── meta │ ├── Dockerfile │ ├── Lavalink.jar │ ├── application.yml │ └── docker-compose.yml └── tsconfig.json ├── tsconfig.base.json ├── tsconfig.eslint.json ├── typedoc.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "audio", 3 | "projectOwner": "skyra-project", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "kyranet", 15 | "name": "Antonio Román", 16 | "avatar_url": "https://avatars0.githubusercontent.com/u/24852502?v=4", 17 | "profile": "https://github.com/kyranet", 18 | "contributions": [ 19 | "code", 20 | "test", 21 | "ideas", 22 | "infra" 23 | ] 24 | }, 25 | { 26 | "login": "Favna", 27 | "name": "Jeroen Claassens", 28 | "avatar_url": "https://avatars3.githubusercontent.com/u/4019718?v=4", 29 | "profile": "https://favware.tech/", 30 | "contributions": [ 31 | "code", 32 | "infra", 33 | "maintenance" 34 | ] 35 | } 36 | ], 37 | "contributorsPerLine": 7 38 | } 39 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{js,ts}] 10 | indent_size = 4 11 | indent_style = tab 12 | block_comment_start = /* 13 | block_comment = * 14 | block_comment_end = */ 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | indent_style = space 19 | 20 | [*.{md,rmd,mkd,mkdn,mdwn,mdown,markdown,litcoffee}] 21 | tab_width = 4 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@sapphire"], 3 | "rules": { 4 | "@typescript-eslint/naming-convention": 0, 5 | "@typescript-eslint/init-declarations": 0, 6 | "@typescript-eslint/ban-types": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.jar filter=lfs diff=lfs merge=lfs -text 2 | * text eol=lf -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /src/ @kyranet @favna 2 | /tests/ @kyranet @favna 3 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | contact@skyra.pw. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **The issue tracker is only for issue reporting or proposals/suggestions. If you have a question, you can find us in our [Discord Server]**. 4 | 5 | To contribute to this repository, feel free to create a new fork of the repository and 6 | submit a pull request. We highly suggest [ESLint] to be installed 7 | in your text editor or IDE of your choice to ensure builds from GitHub Actions do not fail. 8 | 9 | 1. Fork, clone, and select the **main** branch. 10 | 2. Create a new branch in your fork. 11 | 3. Make your changes. 12 | 4. Ensure your linting and tests pass by running `yarn test && yarn lint` 13 | 5. Commit your changes, and push them. 14 | 6. Submit a Pull Request [here]! 15 | 16 | ## @skyra/audio Concept Guidelines 17 | 18 | There are a number of guidelines considered when reviewing Pull Requests to be merged. _This is by no means an exhaustive list, but here are some things to consider before/while submitting your ideas._ 19 | 20 | - Everything in @skyra/audio should be generally useful for the majority of users. Don't let that stop you if you've got a good concept though, as your idea still might be a great addition. 21 | - Everything should follow [OOP paradigms] and generally rely on behaviour over state where possible. This generally helps methods be predictable, keeps the codebase simple and understandable, reduces code duplication through abstraction, and leads to efficiency and therefore scalability. 22 | - Everything should follow our ESLint rules as closely as possible, and should pass lint tests even if you must disable a rule for a single line. 23 | - Scripts that are to be ran outside of the scope of the bot should be added to [scripts] directory and should be in the `.mjs` file format. 24 | 25 | 26 | 27 | [discord server]: https://join.skyra.pw 28 | [here]: https://github.com/skyra-project/audio/pulls 29 | [eslint]: https://eslint.org/ 30 | [oop paradigms]: https://en.wikipedia.org/wiki/Object-oriented_programming 31 | [scripts]: /scripts 32 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kyranet] 4 | patreon: kyranet 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: kyranet 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: https://donate.skyra.pw/paypal 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'bug: ' 5 | labels: 'Bug: Unverified' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | 17 | 1. Write '...' 18 | 2. Click on '...' 19 | 3. See error 20 | 21 | **Expected behavior** 22 | 23 | 24 | 25 | **Screenshots** 26 | 27 | 28 | 29 | **Additional context** 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'request: ' 5 | labels: 'Meta: Feature' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | 17 | **Describe alternatives you've considered** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.0.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you find a vulnerability in @skyra/audio's codebase please report it immediately. 12 | If you deem the vulnerability exploitable by any users in any shape or form please join the Discord server at https://join.skyra.pw and then DM either @kyranet (kyra#0001) or @favna (Favna#0001). 13 | In case the vulnerability is not exploitable by any users you are free to either: 14 | 15 | - Use the GitHub issue tracker to report the issue 16 | - or join the Discord server through https://join.skyra.pw and use the "#feedback" channel. 17 | -------------------------------------------------------------------------------- /.github/husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.github/husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /.github/husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn pretty-quick --staged && yarn lint-staged -------------------------------------------------------------------------------- /.github/problemMatchers/eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "eslint-stylish", 5 | "pattern": [ 6 | { 7 | "regexp": "^([^\\s].*)$", 8 | "file": 1 9 | }, 10 | { 11 | "regexp": "^\\s+(\\d+):(\\d+)\\s+(error|warning|info)\\s+(.*)\\s\\s+(.*)$", 12 | "line": 1, 13 | "column": 2, 14 | "severity": 3, 15 | "message": 4, 16 | "code": 5, 17 | "loop": true 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.github/problemMatchers/tsc.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "tsc", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\s+\\d+\\>)?([^\\s].*)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\)\\s*:\\s+(error|warning|info)\\s+(\\w{1,2}\\d+)\\s*:\\s*(.*)$", 8 | "file": 1, 9 | "location": 2, 10 | "severity": 3, 11 | "code": 4, 12 | "message": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>sapphiredev/readme:sapphire-renovate"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Code Scanning 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '30 1 * * 0' 12 | 13 | jobs: 14 | CodeQL: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3 20 | 21 | - name: Initialize CodeQL 22 | uses: github/codeql-action/init@v1 23 | 24 | - name: Autobuild 25 | uses: github/codeql-action/autobuild@v1 26 | 27 | - name: Perform CodeQL Analysis 28 | uses: github/codeql-action/analyze@v1 29 | -------------------------------------------------------------------------------- /.github/workflows/continuous-delivery.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Delivery 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | Publish: 10 | name: Publish Next to npm 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Project 14 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3 15 | with: 16 | fetch-depth: 0 17 | - name: Add TypeScript problem matcher 18 | run: echo "::add-matcher::.github/problemMatchers/tsc.json" 19 | - name: Use Node.js v16 20 | uses: actions/setup-node@5b52f097d36d4b0b2f94ed6de710023fbb8b2236 # tag=v3 21 | with: 22 | node-version: 16 23 | cache: yarn 24 | - name: Install Dependencies 25 | run: yarn --frozen-lockfile 26 | - name: Bump Version & Publish 27 | run: | 28 | # Resolve the tag to be used. "next" for push events, "pr-{prNumber}" for dispatch events. 29 | TAG=$([[ ${{ github.event_name }} == 'push' ]] && echo 'next' || echo 'pr-${{ github.event.inputs.prNumber }}') 30 | 31 | # Bump the version 32 | yarn standard-version --skip.commit --skip.tag --prerelease "${TAG}.$(git rev-parse --verify --short HEAD)" 33 | 34 | # Publish to NPM 35 | npm publish --tag ${TAG} || true 36 | env: 37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 38 | 39 | Docgen: 40 | name: Docgen 41 | runs-on: ubuntu-latest 42 | if: "github.event_name == 'push'" 43 | steps: 44 | - name: Checkout Project 45 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3 46 | - name: Use Node.js v16 47 | uses: actions/setup-node@5b52f097d36d4b0b2f94ed6de710023fbb8b2236 # tag=v3 48 | with: 49 | node-version: 16 50 | cache: yarn 51 | - name: Install Dependencies 52 | run: yarn --frozen-lockfile 53 | - name: Build documentation 54 | run: yarn docs 55 | - name: Publish Docs 56 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 57 | run: | 58 | REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 59 | 60 | echo -e "\n# Checkout the repo in the target branch" 61 | TARGET_BRANCH="gh-pages" 62 | git clone $REPO out -b $TARGET_BRANCH 63 | 64 | echo -e "\n# Remove any old files in the out folder" 65 | rm -rfv out/assets/* 66 | rm -rfv out/interfaces/* 67 | rm -rfv out/*.html 68 | 69 | echo -e "\n# Move the generated docs to the newly-checked-out repo, to be committed and pushed" 70 | rsync -vaI .all-contributorsrc out/ 71 | rsync -vaI LICENSE.md out/ 72 | rsync -vaI README.md out/ 73 | rsync -vaI docs/ out/ 74 | 75 | echo -e "\n# Commit and push" 76 | cd out 77 | git add --all . 78 | git config user.name "${GITHUB_ACTOR}" 79 | git config user.email "${GITHUB_EMAIL}" 80 | git commit -m "docs: api docs build for ${GITHUB_SHA}" || true 81 | git push origin $TARGET_BRANCH 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.SKYRA_TOKEN }} 84 | GITHUB_ACTOR: NM-EEA-Y 85 | GITHUB_EMAIL: contact@skyra.pw 86 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | Linting: 11 | name: Linting 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Project 15 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3 16 | - name: Add problem matcher 17 | run: echo "::add-matcher::.github/problemMatchers/eslint.json" 18 | - name: Use Node.js v16 19 | uses: actions/setup-node@5b52f097d36d4b0b2f94ed6de710023fbb8b2236 # tag=v3 20 | with: 21 | node-version: 16 22 | cache: yarn 23 | - name: Install Dependencies 24 | run: yarn --frozen-lockfile 25 | - name: Run ESLint 26 | run: yarn lint --fix=false 27 | 28 | Testing: 29 | name: Unit Tests 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout Project 33 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3 34 | - name: Use Node.js v16 35 | uses: actions/setup-node@5b52f097d36d4b0b2f94ed6de710023fbb8b2236 # tag=v3 36 | with: 37 | node-version: 16 38 | cache: yarn 39 | - name: Install Dependencies 40 | run: yarn --frozen-lockfile 41 | - name: Run tests 42 | run: yarn test --coverage 43 | - name: Store code coverage report 44 | uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # tag=v3 45 | with: 46 | name: coverage 47 | path: coverage/ 48 | 49 | Building: 50 | name: Compile source code 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout Project 54 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3 55 | - name: Add problem matcher 56 | run: echo "::add-matcher::.github/problemMatchers/tsc.json" 57 | - name: Use Node.js v16 58 | uses: actions/setup-node@5b52f097d36d4b0b2f94ed6de710023fbb8b2236 # tag=v3 59 | with: 60 | node-version: 16 61 | cache: yarn 62 | - name: Install Dependencies 63 | run: yarn --frozen-lockfile 64 | - name: Build Code 65 | run: yarn build 66 | 67 | Upload_Coverage_Report: 68 | name: Upload coverage report to codecov 69 | needs: [Testing] 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Checkout Project 73 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3 74 | with: 75 | fetch-depth: 2 76 | - name: Download Coverage report 77 | uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v3 78 | with: 79 | name: coverage 80 | path: coverage/ 81 | - name: Codecov Upload 82 | uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # renovate: tag=v2 83 | with: 84 | token: ${{ secrets.CODECOV_TOKEN }} 85 | directory: coverage/ 86 | fail_ci_if_error: true 87 | -------------------------------------------------------------------------------- /.github/workflows/labelsync.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Label Sync 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | label_sync: 10 | name: Automatic Label Synchronization 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Project 14 | uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3 15 | with: 16 | repository: 'sapphiredev/readme' 17 | - name: Run Label Sync 18 | uses: crazy-max/ghaction-github-labeler@52525cb66833763f651fc34e244e4f73b6e07ff5 # renovate: tag=v3 19 | with: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} 21 | yaml-file: .github/labels.yml 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore a blackhole and the folder for development 2 | node_modules/ 3 | .vs/ 4 | .idea/ 5 | *.iml 6 | coverage/ 7 | docs/ 8 | 9 | # Ignore tsc dist folder 10 | dist/ 11 | 12 | # Ignore JavaScript files 13 | **/*.js 14 | **/*.mjs 15 | **/*.js.map 16 | **/*.d.ts 17 | !src/**/*.d.ts 18 | **/*.tsbuildinfo 19 | !jest.config.ts 20 | !scripts/* 21 | 22 | # Ignore heapsnapshot and log files 23 | *.heapsnapshot 24 | *.log 25 | 26 | # Ignore package locks 27 | package-lock.json 28 | 29 | # Ignore the GH cli downloaded by workflows 30 | gh 31 | 32 | # Ignore the "wiki" folder so we can checkout the wiki inside the same folder 33 | wiki/ 34 | # Yarn files 35 | .yarn/install-state.gz 36 | .yarn/build-state.yml 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "name": "Jest", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 9 | "args": ["--runInBand"], 10 | "cwd": "${workspaceFolder}", 11 | "console": "integratedTerminal", 12 | "internalConsoleOptions": "neverOpen", 13 | "disableOptimisticBPs": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["typescript"], 3 | "editor.tabSize": 4, 4 | "editor.useTabStops": true, 5 | "editor.insertSpaces": false, 6 | "editor.detectIndentation": false, 7 | "files.eol": "\n", 8 | "deno.enable": false 9 | } 10 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-typescript.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-typescript", 5 | factory: function (require) { 6 | var plugin=(()=>{var Ft=Object.create,H=Object.defineProperty,Bt=Object.defineProperties,Kt=Object.getOwnPropertyDescriptor,zt=Object.getOwnPropertyDescriptors,Gt=Object.getOwnPropertyNames,Q=Object.getOwnPropertySymbols,$t=Object.getPrototypeOf,ne=Object.prototype.hasOwnProperty,De=Object.prototype.propertyIsEnumerable;var Re=(e,t,r)=>t in e?H(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,u=(e,t)=>{for(var r in t||(t={}))ne.call(t,r)&&Re(e,r,t[r]);if(Q)for(var r of Q(t))De.call(t,r)&&Re(e,r,t[r]);return e},g=(e,t)=>Bt(e,zt(t)),Lt=e=>H(e,"__esModule",{value:!0});var R=(e,t)=>{var r={};for(var s in e)ne.call(e,s)&&t.indexOf(s)<0&&(r[s]=e[s]);if(e!=null&&Q)for(var s of Q(e))t.indexOf(s)<0&&De.call(e,s)&&(r[s]=e[s]);return r};var I=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Vt=(e,t)=>{for(var r in t)H(e,r,{get:t[r],enumerable:!0})},Qt=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of Gt(t))!ne.call(e,s)&&s!=="default"&&H(e,s,{get:()=>t[s],enumerable:!(r=Kt(t,s))||r.enumerable});return e},C=e=>Qt(Lt(H(e!=null?Ft($t(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var xe=I(J=>{"use strict";Object.defineProperty(J,"__esModule",{value:!0});function _(e){let t=[...e.caches],r=t.shift();return r===void 0?ve():{get(s,n,a={miss:()=>Promise.resolve()}){return r.get(s,n,a).catch(()=>_({caches:t}).get(s,n,a))},set(s,n){return r.set(s,n).catch(()=>_({caches:t}).set(s,n))},delete(s){return r.delete(s).catch(()=>_({caches:t}).delete(s))},clear(){return r.clear().catch(()=>_({caches:t}).clear())}}}function ve(){return{get(e,t,r={miss:()=>Promise.resolve()}){return t().then(n=>Promise.all([n,r.miss(n)])).then(([n])=>n)},set(e,t){return Promise.resolve(t)},delete(e){return Promise.resolve()},clear(){return Promise.resolve()}}}J.createFallbackableCache=_;J.createNullCache=ve});var Ee=I(($s,qe)=>{qe.exports=xe()});var Te=I(ae=>{"use strict";Object.defineProperty(ae,"__esModule",{value:!0});function Jt(e={serializable:!0}){let t={};return{get(r,s,n={miss:()=>Promise.resolve()}){let a=JSON.stringify(r);if(a in t)return Promise.resolve(e.serializable?JSON.parse(t[a]):t[a]);let o=s(),d=n&&n.miss||(()=>Promise.resolve());return o.then(y=>d(y)).then(()=>o)},set(r,s){return t[JSON.stringify(r)]=e.serializable?JSON.stringify(s):s,Promise.resolve(s)},delete(r){return delete t[JSON.stringify(r)],Promise.resolve()},clear(){return t={},Promise.resolve()}}}ae.createInMemoryCache=Jt});var we=I((Vs,Me)=>{Me.exports=Te()});var Ce=I(M=>{"use strict";Object.defineProperty(M,"__esModule",{value:!0});function Xt(e,t,r){let s={"x-algolia-api-key":r,"x-algolia-application-id":t};return{headers(){return e===oe.WithinHeaders?s:{}},queryParameters(){return e===oe.WithinQueryParameters?s:{}}}}function Yt(e){let t=0,r=()=>(t++,new Promise(s=>{setTimeout(()=>{s(e(r))},Math.min(100*t,1e3))}));return e(r)}function ke(e,t=(r,s)=>Promise.resolve()){return Object.assign(e,{wait(r){return ke(e.then(s=>Promise.all([t(s,r),s])).then(s=>s[1]))}})}function Zt(e){let t=e.length-1;for(t;t>0;t--){let r=Math.floor(Math.random()*(t+1)),s=e[t];e[t]=e[r],e[r]=s}return e}function er(e,t){return Object.keys(t!==void 0?t:{}).forEach(r=>{e[r]=t[r](e)}),e}function tr(e,...t){let r=0;return e.replace(/%s/g,()=>encodeURIComponent(t[r++]))}var rr="4.2.0",sr=e=>()=>e.transporter.requester.destroy(),oe={WithinQueryParameters:0,WithinHeaders:1};M.AuthMode=oe;M.addMethods=er;M.createAuth=Xt;M.createRetryablePromise=Yt;M.createWaitablePromise=ke;M.destroy=sr;M.encode=tr;M.shuffle=Zt;M.version=rr});var F=I((Js,Ue)=>{Ue.exports=Ce()});var Ne=I(ie=>{"use strict";Object.defineProperty(ie,"__esModule",{value:!0});var nr={Delete:"DELETE",Get:"GET",Post:"POST",Put:"PUT"};ie.MethodEnum=nr});var B=I((Ys,We)=>{We.exports=Ne()});var Ze=I(A=>{"use strict";Object.defineProperty(A,"__esModule",{value:!0});var He=B();function ce(e,t){let r=e||{},s=r.data||{};return Object.keys(r).forEach(n=>{["timeout","headers","queryParameters","data","cacheable"].indexOf(n)===-1&&(s[n]=r[n])}),{data:Object.entries(s).length>0?s:void 0,timeout:r.timeout||t,headers:r.headers||{},queryParameters:r.queryParameters||{},cacheable:r.cacheable}}var X={Read:1,Write:2,Any:3},U={Up:1,Down:2,Timeouted:3},_e=2*60*1e3;function ue(e,t=U.Up){return g(u({},e),{status:t,lastUpdate:Date.now()})}function Fe(e){return e.status===U.Up||Date.now()-e.lastUpdate>_e}function Be(e){return e.status===U.Timeouted&&Date.now()-e.lastUpdate<=_e}function le(e){return{protocol:e.protocol||"https",url:e.url,accept:e.accept||X.Any}}function ar(e,t){return Promise.all(t.map(r=>e.get(r,()=>Promise.resolve(ue(r))))).then(r=>{let s=r.filter(d=>Fe(d)),n=r.filter(d=>Be(d)),a=[...s,...n],o=a.length>0?a.map(d=>le(d)):t;return{getTimeout(d,y){return(n.length===0&&d===0?1:n.length+3+d)*y},statelessHosts:o}})}var or=({isTimedOut:e,status:t})=>!e&&~~t==0,ir=e=>{let t=e.status;return e.isTimedOut||or(e)||~~(t/100)!=2&&~~(t/100)!=4},cr=({status:e})=>~~(e/100)==2,ur=(e,t)=>ir(e)?t.onRetry(e):cr(e)?t.onSucess(e):t.onFail(e);function Qe(e,t,r,s){let n=[],a=$e(r,s),o=Le(e,s),d=r.method,y=r.method!==He.MethodEnum.Get?{}:u(u({},r.data),s.data),b=u(u(u({"x-algolia-agent":e.userAgent.value},e.queryParameters),y),s.queryParameters),f=0,p=(h,S)=>{let O=h.pop();if(O===void 0)throw Ve(de(n));let P={data:a,headers:o,method:d,url:Ge(O,r.path,b),connectTimeout:S(f,e.timeouts.connect),responseTimeout:S(f,s.timeout)},x=j=>{let T={request:P,response:j,host:O,triesLeft:h.length};return n.push(T),T},v={onSucess:j=>Ke(j),onRetry(j){let T=x(j);return j.isTimedOut&&f++,Promise.all([e.logger.info("Retryable failure",pe(T)),e.hostsCache.set(O,ue(O,j.isTimedOut?U.Timeouted:U.Down))]).then(()=>p(h,S))},onFail(j){throw x(j),ze(j,de(n))}};return e.requester.send(P).then(j=>ur(j,v))};return ar(e.hostsCache,t).then(h=>p([...h.statelessHosts].reverse(),h.getTimeout))}function lr(e){let{hostsCache:t,logger:r,requester:s,requestsCache:n,responsesCache:a,timeouts:o,userAgent:d,hosts:y,queryParameters:b,headers:f}=e,p={hostsCache:t,logger:r,requester:s,requestsCache:n,responsesCache:a,timeouts:o,userAgent:d,headers:f,queryParameters:b,hosts:y.map(h=>le(h)),read(h,S){let O=ce(S,p.timeouts.read),P=()=>Qe(p,p.hosts.filter(j=>(j.accept&X.Read)!=0),h,O);if((O.cacheable!==void 0?O.cacheable:h.cacheable)!==!0)return P();let v={request:h,mappedRequestOptions:O,transporter:{queryParameters:p.queryParameters,headers:p.headers}};return p.responsesCache.get(v,()=>p.requestsCache.get(v,()=>p.requestsCache.set(v,P()).then(j=>Promise.all([p.requestsCache.delete(v),j]),j=>Promise.all([p.requestsCache.delete(v),Promise.reject(j)])).then(([j,T])=>T)),{miss:j=>p.responsesCache.set(v,j)})},write(h,S){return Qe(p,p.hosts.filter(O=>(O.accept&X.Write)!=0),h,ce(S,p.timeouts.write))}};return p}function dr(e){let t={value:`Algolia for JavaScript (${e})`,add(r){let s=`; ${r.segment}${r.version!==void 0?` (${r.version})`:""}`;return t.value.indexOf(s)===-1&&(t.value=`${t.value}${s}`),t}};return t}function Ke(e){try{return JSON.parse(e.content)}catch(t){throw Je(t.message,e)}}function ze({content:e,status:t},r){let s=e;try{s=JSON.parse(e).message}catch(n){}return Xe(s,t,r)}function pr(e,...t){let r=0;return e.replace(/%s/g,()=>encodeURIComponent(t[r++]))}function Ge(e,t,r){let s=Ye(r),n=`${e.protocol}://${e.url}/${t.charAt(0)==="/"?t.substr(1):t}`;return s.length&&(n+=`?${s}`),n}function Ye(e){let t=r=>Object.prototype.toString.call(r)==="[object Object]"||Object.prototype.toString.call(r)==="[object Array]";return Object.keys(e).map(r=>pr("%s=%s",r,t(e[r])?JSON.stringify(e[r]):e[r])).join("&")}function $e(e,t){if(e.method===He.MethodEnum.Get||e.data===void 0&&t.data===void 0)return;let r=Array.isArray(e.data)?e.data:u(u({},e.data),t.data);return JSON.stringify(r)}function Le(e,t){let r=u(u({},e.headers),t.headers),s={};return Object.keys(r).forEach(n=>{let a=r[n];s[n.toLowerCase()]=a}),s}function de(e){return e.map(t=>pe(t))}function pe(e){let t=e.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return g(u({},e),{request:g(u({},e.request),{headers:u(u({},e.request.headers),t)})})}function Xe(e,t,r){return{name:"ApiError",message:e,status:t,transporterStackTrace:r}}function Je(e,t){return{name:"DeserializationError",message:e,response:t}}function Ve(e){return{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:e}}A.CallEnum=X;A.HostStatusEnum=U;A.createApiError=Xe;A.createDeserializationError=Je;A.createMappedRequestOptions=ce;A.createRetryError=Ve;A.createStatefulHost=ue;A.createStatelessHost=le;A.createTransporter=lr;A.createUserAgent=dr;A.deserializeFailure=ze;A.deserializeSuccess=Ke;A.isStatefulHostTimeouted=Be;A.isStatefulHostUp=Fe;A.serializeData=$e;A.serializeHeaders=Le;A.serializeQueryParameters=Ye;A.serializeUrl=Ge;A.stackFrameWithoutCredentials=pe;A.stackTraceWithoutCredentials=de});var K=I((en,et)=>{et.exports=Ze()});var tt=I(w=>{"use strict";Object.defineProperty(w,"__esModule",{value:!0});var N=F(),mr=K(),z=B(),hr=e=>{let t=e.region||"us",r=N.createAuth(N.AuthMode.WithinHeaders,e.appId,e.apiKey),s=mr.createTransporter(g(u({hosts:[{url:`analytics.${t}.algolia.com`}]},e),{headers:u(g(u({},r.headers()),{"content-type":"application/json"}),e.headers),queryParameters:u(u({},r.queryParameters()),e.queryParameters)})),n=e.appId;return N.addMethods({appId:n,transporter:s},e.methods)},yr=e=>(t,r)=>e.transporter.write({method:z.MethodEnum.Post,path:"2/abtests",data:t},r),gr=e=>(t,r)=>e.transporter.write({method:z.MethodEnum.Delete,path:N.encode("2/abtests/%s",t)},r),fr=e=>(t,r)=>e.transporter.read({method:z.MethodEnum.Get,path:N.encode("2/abtests/%s",t)},r),br=e=>t=>e.transporter.read({method:z.MethodEnum.Get,path:"2/abtests"},t),Pr=e=>(t,r)=>e.transporter.write({method:z.MethodEnum.Post,path:N.encode("2/abtests/%s/stop",t)},r);w.addABTest=yr;w.createAnalyticsClient=hr;w.deleteABTest=gr;w.getABTest=fr;w.getABTests=br;w.stopABTest=Pr});var st=I((rn,rt)=>{rt.exports=tt()});var at=I(G=>{"use strict";Object.defineProperty(G,"__esModule",{value:!0});var me=F(),jr=K(),nt=B(),Or=e=>{let t=e.region||"us",r=me.createAuth(me.AuthMode.WithinHeaders,e.appId,e.apiKey),s=jr.createTransporter(g(u({hosts:[{url:`recommendation.${t}.algolia.com`}]},e),{headers:u(g(u({},r.headers()),{"content-type":"application/json"}),e.headers),queryParameters:u(u({},r.queryParameters()),e.queryParameters)}));return me.addMethods({appId:e.appId,transporter:s},e.methods)},Ir=e=>t=>e.transporter.read({method:nt.MethodEnum.Get,path:"1/strategies/personalization"},t),Ar=e=>(t,r)=>e.transporter.write({method:nt.MethodEnum.Post,path:"1/strategies/personalization",data:t},r);G.createRecommendationClient=Or;G.getPersonalizationStrategy=Ir;G.setPersonalizationStrategy=Ar});var it=I((nn,ot)=>{ot.exports=at()});var jt=I(i=>{"use strict";Object.defineProperty(i,"__esModule",{value:!0});var l=F(),q=K(),m=B(),Sr=require("crypto");function Y(e){let t=r=>e.request(r).then(s=>{if(e.batch!==void 0&&e.batch(s.hits),!e.shouldStop(s))return s.cursor?t({cursor:s.cursor}):t({page:(r.page||0)+1})});return t({})}var Dr=e=>{let t=e.appId,r=l.createAuth(e.authMode!==void 0?e.authMode:l.AuthMode.WithinHeaders,t,e.apiKey),s=q.createTransporter(g(u({hosts:[{url:`${t}-dsn.algolia.net`,accept:q.CallEnum.Read},{url:`${t}.algolia.net`,accept:q.CallEnum.Write}].concat(l.shuffle([{url:`${t}-1.algolianet.com`},{url:`${t}-2.algolianet.com`},{url:`${t}-3.algolianet.com`}]))},e),{headers:u(g(u({},r.headers()),{"content-type":"application/x-www-form-urlencoded"}),e.headers),queryParameters:u(u({},r.queryParameters()),e.queryParameters)})),n={transporter:s,appId:t,addAlgoliaAgent(a,o){s.userAgent.add({segment:a,version:o})},clearCache(){return Promise.all([s.requestsCache.clear(),s.responsesCache.clear()]).then(()=>{})}};return l.addMethods(n,e.methods)};function ct(){return{name:"MissingObjectIDError",message:"All objects must have an unique objectID (like a primary key) to be valid. Algolia is also able to generate objectIDs automatically but *it's not recommended*. To do it, use the `{'autoGenerateObjectIDIfNotExist': true}` option."}}function ut(){return{name:"ObjectNotFoundError",message:"Object not found."}}function lt(){return{name:"ValidUntilNotFoundError",message:"ValidUntil not found in given secured api key."}}var Rr=e=>(t,r)=>{let d=r||{},{queryParameters:s}=d,n=R(d,["queryParameters"]),a=u({acl:t},s!==void 0?{queryParameters:s}:{}),o=(y,b)=>l.createRetryablePromise(f=>$(e)(y.key,b).catch(p=>{if(p.status!==404)throw p;return f()}));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:"1/keys",data:a},n),o)},vr=e=>(t,r,s)=>{let n=q.createMappedRequestOptions(s);return n.queryParameters["X-Algolia-User-ID"]=t,e.transporter.write({method:m.MethodEnum.Post,path:"1/clusters/mapping",data:{cluster:r}},n)},xr=e=>(t,r,s)=>e.transporter.write({method:m.MethodEnum.Post,path:"1/clusters/mapping/batch",data:{users:t,cluster:r}},s),Z=e=>(t,r,s)=>{let n=(a,o)=>L(e)(t,{methods:{waitTask:D}}).waitTask(a.taskID,o);return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/operation",t),data:{operation:"copy",destination:r}},s),n)},qr=e=>(t,r,s)=>Z(e)(t,r,g(u({},s),{scope:[ee.Rules]})),Er=e=>(t,r,s)=>Z(e)(t,r,g(u({},s),{scope:[ee.Settings]})),Tr=e=>(t,r,s)=>Z(e)(t,r,g(u({},s),{scope:[ee.Synonyms]})),Mr=e=>(t,r)=>{let s=(n,a)=>l.createRetryablePromise(o=>$(e)(t,a).then(o).catch(d=>{if(d.status!==404)throw d}));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Delete,path:l.encode("1/keys/%s",t)},r),s)},wr=()=>(e,t)=>{let r=q.serializeQueryParameters(t),s=Sr.createHmac("sha256",e).update(r).digest("hex");return Buffer.from(s+r).toString("base64")},$=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/keys/%s",t)},r),kr=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/logs"},t),Cr=()=>e=>{let t=Buffer.from(e,"base64").toString("ascii"),r=/validUntil=(\d+)/,s=t.match(r);if(s===null)throw lt();return parseInt(s[1],10)-Math.round(new Date().getTime()/1e3)},Ur=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/clusters/mapping/top"},t),Nr=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/clusters/mapping/%s",t)},r),Wr=e=>t=>{let n=t||{},{retrieveMappings:r}=n,s=R(n,["retrieveMappings"]);return r===!0&&(s.getClusters=!0),e.transporter.read({method:m.MethodEnum.Get,path:"1/clusters/mapping/pending"},s)},L=e=>(t,r={})=>{let s={transporter:e.transporter,appId:e.appId,indexName:t};return l.addMethods(s,r.methods)},Hr=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/keys"},t),_r=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/clusters"},t),Fr=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/indexes"},t),Br=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/clusters/mapping"},t),Kr=e=>(t,r,s)=>{let n=(a,o)=>L(e)(t,{methods:{waitTask:D}}).waitTask(a.taskID,o);return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/operation",t),data:{operation:"move",destination:r}},s),n)},zr=e=>(t,r)=>{let s=(n,a)=>Promise.all(Object.keys(n.taskID).map(o=>L(e)(o,{methods:{waitTask:D}}).waitTask(n.taskID[o],a)));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:"1/indexes/*/batch",data:{requests:t}},r),s)},Gr=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:"1/indexes/*/objects",data:{requests:t}},r),$r=e=>(t,r)=>{let s=t.map(n=>g(u({},n),{params:q.serializeQueryParameters(n.params||{})}));return e.transporter.read({method:m.MethodEnum.Post,path:"1/indexes/*/queries",data:{requests:s},cacheable:!0},r)},Lr=e=>(t,r)=>Promise.all(t.map(s=>{let d=s.params,{facetName:n,facetQuery:a}=d,o=R(d,["facetName","facetQuery"]);return L(e)(s.indexName,{methods:{searchForFacetValues:dt}}).searchForFacetValues(n,a,u(u({},r),o))})),Vr=e=>(t,r)=>{let s=q.createMappedRequestOptions(r);return s.queryParameters["X-Algolia-User-ID"]=t,e.transporter.write({method:m.MethodEnum.Delete,path:"1/clusters/mapping"},s)},Qr=e=>(t,r)=>{let s=(n,a)=>l.createRetryablePromise(o=>$(e)(t,a).catch(d=>{if(d.status!==404)throw d;return o()}));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/keys/%s/restore",t)},r),s)},Jr=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:"1/clusters/mapping/search",data:{query:t}},r),Xr=e=>(t,r)=>{let s=Object.assign({},r),f=r||{},{queryParameters:n}=f,a=R(f,["queryParameters"]),o=n?{queryParameters:n}:{},d=["acl","indexes","referers","restrictSources","queryParameters","description","maxQueriesPerIPPerHour","maxHitsPerQuery"],y=p=>Object.keys(s).filter(h=>d.indexOf(h)!==-1).every(h=>p[h]===s[h]),b=(p,h)=>l.createRetryablePromise(S=>$(e)(t,h).then(O=>y(O)?Promise.resolve():S()));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Put,path:l.encode("1/keys/%s",t),data:o},a),b)},pt=e=>(t,r)=>{let s=(n,a)=>D(e)(n.taskID,a);return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/batch",e.indexName),data:{requests:t}},r),s)},Yr=e=>t=>Y(g(u({},t),{shouldStop:r=>r.cursor===void 0,request:r=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/browse",e.indexName),data:r},t)})),Zr=e=>t=>{let r=u({hitsPerPage:1e3},t);return Y(g(u({},r),{shouldStop:s=>s.hits.lengthg(u({},n),{hits:n.hits.map(a=>(delete a._highlightResult,a))}))}}))},es=e=>t=>{let r=u({hitsPerPage:1e3},t);return Y(g(u({},r),{shouldStop:s=>s.hits.lengthg(u({},n),{hits:n.hits.map(a=>(delete a._highlightResult,a))}))}}))},te=e=>(t,r,s)=>{let y=s||{},{batchSize:n}=y,a=R(y,["batchSize"]),o={taskIDs:[],objectIDs:[]},d=(b=0)=>{let f=[],p;for(p=b;p({action:r,body:h})),a).then(h=>(o.objectIDs=o.objectIDs.concat(h.objectIDs),o.taskIDs.push(h.taskID),p++,d(p)))};return l.createWaitablePromise(d(),(b,f)=>Promise.all(b.taskIDs.map(p=>D(e)(p,f))))},ts=e=>t=>l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/clear",e.indexName)},t),(r,s)=>D(e)(r.taskID,s)),rs=e=>t=>{let a=t||{},{forwardToReplicas:r}=a,s=R(a,["forwardToReplicas"]),n=q.createMappedRequestOptions(s);return r&&(n.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/rules/clear",e.indexName)},n),(o,d)=>D(e)(o.taskID,d))},ss=e=>t=>{let a=t||{},{forwardToReplicas:r}=a,s=R(a,["forwardToReplicas"]),n=q.createMappedRequestOptions(s);return r&&(n.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/synonyms/clear",e.indexName)},n),(o,d)=>D(e)(o.taskID,d))},ns=e=>(t,r)=>l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/deleteByQuery",e.indexName),data:t},r),(s,n)=>D(e)(s.taskID,n)),as=e=>t=>l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Delete,path:l.encode("1/indexes/%s",e.indexName)},t),(r,s)=>D(e)(r.taskID,s)),os=e=>(t,r)=>l.createWaitablePromise(yt(e)([t],r).then(s=>({taskID:s.taskIDs[0]})),(s,n)=>D(e)(s.taskID,n)),yt=e=>(t,r)=>{let s=t.map(n=>({objectID:n}));return te(e)(s,k.DeleteObject,r)},is=e=>(t,r)=>{let o=r||{},{forwardToReplicas:s}=o,n=R(o,["forwardToReplicas"]),a=q.createMappedRequestOptions(n);return s&&(a.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Delete,path:l.encode("1/indexes/%s/rules/%s",e.indexName,t)},a),(d,y)=>D(e)(d.taskID,y))},cs=e=>(t,r)=>{let o=r||{},{forwardToReplicas:s}=o,n=R(o,["forwardToReplicas"]),a=q.createMappedRequestOptions(n);return s&&(a.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Delete,path:l.encode("1/indexes/%s/synonyms/%s",e.indexName,t)},a),(d,y)=>D(e)(d.taskID,y))},us=e=>t=>gt(e)(t).then(()=>!0).catch(r=>{if(r.status!==404)throw r;return!1}),ls=e=>(t,r)=>{let y=r||{},{query:s,paginate:n}=y,a=R(y,["query","paginate"]),o=0,d=()=>ft(e)(s||"",g(u({},a),{page:o})).then(b=>{for(let[f,p]of Object.entries(b.hits))if(t(p))return{object:p,position:parseInt(f,10),page:o};if(o++,n===!1||o>=b.nbPages)throw ut();return d()});return d()},ds=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/%s",e.indexName,t)},r),ps=()=>(e,t)=>{for(let[r,s]of Object.entries(e.hits))if(s.objectID===t)return parseInt(r,10);return-1},ms=e=>(t,r)=>{let o=r||{},{attributesToRetrieve:s}=o,n=R(o,["attributesToRetrieve"]),a=t.map(d=>u({indexName:e.indexName,objectID:d},s?{attributesToRetrieve:s}:{}));return e.transporter.read({method:m.MethodEnum.Post,path:"1/indexes/*/objects",data:{requests:a}},n)},hs=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/rules/%s",e.indexName,t)},r),gt=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/settings",e.indexName),data:{getVersion:2}},t),ys=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/synonyms/%s",e.indexName,t)},r),bt=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/task/%s",e.indexName,t.toString())},r),gs=e=>(t,r)=>l.createWaitablePromise(Pt(e)([t],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,n)=>D(e)(s.taskID,n)),Pt=e=>(t,r)=>{let o=r||{},{createIfNotExists:s}=o,n=R(o,["createIfNotExists"]),a=s?k.PartialUpdateObject:k.PartialUpdateObjectNoCreate;return te(e)(t,a,n)},fs=e=>(t,r)=>{let O=r||{},{safe:s,autoGenerateObjectIDIfNotExist:n,batchSize:a}=O,o=R(O,["safe","autoGenerateObjectIDIfNotExist","batchSize"]),d=(P,x,v,j)=>l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/operation",P),data:{operation:v,destination:x}},j),(T,V)=>D(e)(T.taskID,V)),y=Math.random().toString(36).substring(7),b=`${e.indexName}_tmp_${y}`,f=he({appId:e.appId,transporter:e.transporter,indexName:b}),p=[],h=d(e.indexName,b,"copy",g(u({},o),{scope:["settings","synonyms","rules"]}));p.push(h);let S=(s?h.wait(o):h).then(()=>{let P=f(t,g(u({},o),{autoGenerateObjectIDIfNotExist:n,batchSize:a}));return p.push(P),s?P.wait(o):P}).then(()=>{let P=d(b,e.indexName,"move",o);return p.push(P),s?P.wait(o):P}).then(()=>Promise.all(p)).then(([P,x,v])=>({objectIDs:x.objectIDs,taskIDs:[P.taskID,...x.taskIDs,v.taskID]}));return l.createWaitablePromise(S,(P,x)=>Promise.all(p.map(v=>v.wait(x))))},bs=e=>(t,r)=>ye(e)(t,g(u({},r),{clearExistingRules:!0})),Ps=e=>(t,r)=>ge(e)(t,g(u({},r),{replaceExistingSynonyms:!0})),js=e=>(t,r)=>l.createWaitablePromise(he(e)([t],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,n)=>D(e)(s.taskID,n)),he=e=>(t,r)=>{let o=r||{},{autoGenerateObjectIDIfNotExist:s}=o,n=R(o,["autoGenerateObjectIDIfNotExist"]),a=s?k.AddObject:k.UpdateObject;if(a===k.UpdateObject){for(let d of t)if(d.objectID===void 0)return l.createWaitablePromise(Promise.reject(ct()))}return te(e)(t,a,n)},Os=e=>(t,r)=>ye(e)([t],r),ye=e=>(t,r)=>{let d=r||{},{forwardToReplicas:s,clearExistingRules:n}=d,a=R(d,["forwardToReplicas","clearExistingRules"]),o=q.createMappedRequestOptions(a);return s&&(o.queryParameters.forwardToReplicas=1),n&&(o.queryParameters.clearExistingRules=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/rules/batch",e.indexName),data:t},o),(y,b)=>D(e)(y.taskID,b))},Is=e=>(t,r)=>ge(e)([t],r),ge=e=>(t,r)=>{let d=r||{},{forwardToReplicas:s,replaceExistingSynonyms:n}=d,a=R(d,["forwardToReplicas","replaceExistingSynonyms"]),o=q.createMappedRequestOptions(a);return s&&(o.queryParameters.forwardToReplicas=1),n&&(o.queryParameters.replaceExistingSynonyms=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/synonyms/batch",e.indexName),data:t},o),(y,b)=>D(e)(y.taskID,b))},ft=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/query",e.indexName),data:{query:t},cacheable:!0},r),dt=e=>(t,r,s)=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/facets/%s/query",e.indexName,t),data:{facetQuery:r},cacheable:!0},s),mt=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/rules/search",e.indexName),data:{query:t}},r),ht=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/synonyms/search",e.indexName),data:{query:t}},r),As=e=>(t,r)=>{let o=r||{},{forwardToReplicas:s}=o,n=R(o,["forwardToReplicas"]),a=q.createMappedRequestOptions(n);return s&&(a.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Put,path:l.encode("1/indexes/%s/settings",e.indexName),data:t},a),(d,y)=>D(e)(d.taskID,y))},D=e=>(t,r)=>l.createRetryablePromise(s=>bt(e)(t,r).then(n=>n.status!=="published"?s():void 0)),Ss={AddObject:"addObject",Analytics:"analytics",Browser:"browse",DeleteIndex:"deleteIndex",DeleteObject:"deleteObject",EditSettings:"editSettings",ListIndexes:"listIndexes",Logs:"logs",Recommendation:"recommendation",Search:"search",SeeUnretrievableAttributes:"seeUnretrievableAttributes",Settings:"settings",Usage:"usage"},k={AddObject:"addObject",UpdateObject:"updateObject",PartialUpdateObject:"partialUpdateObject",PartialUpdateObjectNoCreate:"partialUpdateObjectNoCreate",DeleteObject:"deleteObject"},ee={Settings:"settings",Synonyms:"synonyms",Rules:"rules"},Ds={None:"none",StopIfEnoughMatches:"stopIfEnoughMatches"},Rs={Synonym:"synonym",OneWaySynonym:"oneWaySynonym",AltCorrection1:"altCorrection1",AltCorrection2:"altCorrection2",Placeholder:"placeholder"};i.ApiKeyACLEnum=Ss;i.BatchActionEnum=k;i.ScopeEnum=ee;i.StrategyEnum=Ds;i.SynonymEnum=Rs;i.addApiKey=Rr;i.assignUserID=vr;i.assignUserIDs=xr;i.batch=pt;i.browseObjects=Yr;i.browseRules=Zr;i.browseSynonyms=es;i.chunkedBatch=te;i.clearObjects=ts;i.clearRules=rs;i.clearSynonyms=ss;i.copyIndex=Z;i.copyRules=qr;i.copySettings=Er;i.copySynonyms=Tr;i.createBrowsablePromise=Y;i.createMissingObjectIDError=ct;i.createObjectNotFoundError=ut;i.createSearchClient=Dr;i.createValidUntilNotFoundError=lt;i.deleteApiKey=Mr;i.deleteBy=ns;i.deleteIndex=as;i.deleteObject=os;i.deleteObjects=yt;i.deleteRule=is;i.deleteSynonym=cs;i.exists=us;i.findObject=ls;i.generateSecuredApiKey=wr;i.getApiKey=$;i.getLogs=kr;i.getObject=ds;i.getObjectPosition=ps;i.getObjects=ms;i.getRule=hs;i.getSecuredApiKeyRemainingValidity=Cr;i.getSettings=gt;i.getSynonym=ys;i.getTask=bt;i.getTopUserIDs=Ur;i.getUserID=Nr;i.hasPendingMappings=Wr;i.initIndex=L;i.listApiKeys=Hr;i.listClusters=_r;i.listIndices=Fr;i.listUserIDs=Br;i.moveIndex=Kr;i.multipleBatch=zr;i.multipleGetObjects=Gr;i.multipleQueries=$r;i.multipleSearchForFacetValues=Lr;i.partialUpdateObject=gs;i.partialUpdateObjects=Pt;i.removeUserID=Vr;i.replaceAllObjects=fs;i.replaceAllRules=bs;i.replaceAllSynonyms=Ps;i.restoreApiKey=Qr;i.saveObject=js;i.saveObjects=he;i.saveRule=Os;i.saveRules=ye;i.saveSynonym=Is;i.saveSynonyms=ge;i.search=ft;i.searchForFacetValues=dt;i.searchRules=mt;i.searchSynonyms=ht;i.searchUserIDs=Jr;i.setSettings=As;i.updateApiKey=Xr;i.waitTask=D});var It=I((on,Ot)=>{Ot.exports=jt()});var At=I(re=>{"use strict";Object.defineProperty(re,"__esModule",{value:!0});function vs(){return{debug(e,t){return Promise.resolve()},info(e,t){return Promise.resolve()},error(e,t){return Promise.resolve()}}}var xs={Debug:1,Info:2,Error:3};re.LogLevelEnum=xs;re.createNullLogger=vs});var Dt=I((un,St)=>{St.exports=At()});var xt=I(fe=>{"use strict";Object.defineProperty(fe,"__esModule",{value:!0});var Rt=require("http"),vt=require("https"),qs=require("url");function Es(){let e={keepAlive:!0},t=new Rt.Agent(e),r=new vt.Agent(e);return{send(s){return new Promise(n=>{let a=qs.parse(s.url),o=a.query===null?a.pathname:`${a.pathname}?${a.query}`,d=u({agent:a.protocol==="https:"?r:t,hostname:a.hostname,path:o,method:s.method,headers:s.headers},a.port!==void 0?{port:a.port||""}:{}),y=(a.protocol==="https:"?vt:Rt).request(d,h=>{let S="";h.on("data",O=>S+=O),h.on("end",()=>{clearTimeout(f),clearTimeout(p),n({status:h.statusCode||0,content:S,isTimedOut:!1})})}),b=(h,S)=>setTimeout(()=>{y.abort(),n({status:0,content:S,isTimedOut:!0})},h*1e3),f=b(s.connectTimeout,"Connection timeout"),p;y.on("error",h=>{clearTimeout(f),clearTimeout(p),n({status:0,content:h.message,isTimedOut:!1})}),y.once("response",()=>{clearTimeout(f),p=b(s.responseTimeout,"Socket timeout")}),s.data!==void 0&&y.write(s.data),y.end()})},destroy(){return t.destroy(),r.destroy(),Promise.resolve()}}}fe.createNodeHttpRequester=Es});var Et=I((dn,qt)=>{qt.exports=xt()});var kt=I((pn,Tt)=>{"use strict";var Mt=Ee(),Ts=we(),W=st(),be=F(),Pe=it(),c=It(),Ms=Dt(),ws=Et(),ks=K();function wt(e,t,r){let s={appId:e,apiKey:t,timeouts:{connect:2,read:5,write:30},requester:ws.createNodeHttpRequester(),logger:Ms.createNullLogger(),responsesCache:Mt.createNullCache(),requestsCache:Mt.createNullCache(),hostsCache:Ts.createInMemoryCache(),userAgent:ks.createUserAgent(be.version).add({segment:"Node.js",version:process.versions.node})};return c.createSearchClient(g(u(u({},s),r),{methods:{search:c.multipleQueries,searchForFacetValues:c.multipleSearchForFacetValues,multipleBatch:c.multipleBatch,multipleGetObjects:c.multipleGetObjects,multipleQueries:c.multipleQueries,copyIndex:c.copyIndex,copySettings:c.copySettings,copyRules:c.copyRules,copySynonyms:c.copySynonyms,moveIndex:c.moveIndex,listIndices:c.listIndices,getLogs:c.getLogs,listClusters:c.listClusters,multipleSearchForFacetValues:c.multipleSearchForFacetValues,getApiKey:c.getApiKey,addApiKey:c.addApiKey,listApiKeys:c.listApiKeys,updateApiKey:c.updateApiKey,deleteApiKey:c.deleteApiKey,restoreApiKey:c.restoreApiKey,assignUserID:c.assignUserID,assignUserIDs:c.assignUserIDs,getUserID:c.getUserID,searchUserIDs:c.searchUserIDs,listUserIDs:c.listUserIDs,getTopUserIDs:c.getTopUserIDs,removeUserID:c.removeUserID,hasPendingMappings:c.hasPendingMappings,generateSecuredApiKey:c.generateSecuredApiKey,getSecuredApiKeyRemainingValidity:c.getSecuredApiKeyRemainingValidity,destroy:be.destroy,initIndex:n=>a=>c.initIndex(n)(a,{methods:{batch:c.batch,delete:c.deleteIndex,getObject:c.getObject,getObjects:c.getObjects,saveObject:c.saveObject,saveObjects:c.saveObjects,search:c.search,searchForFacetValues:c.searchForFacetValues,waitTask:c.waitTask,setSettings:c.setSettings,getSettings:c.getSettings,partialUpdateObject:c.partialUpdateObject,partialUpdateObjects:c.partialUpdateObjects,deleteObject:c.deleteObject,deleteObjects:c.deleteObjects,deleteBy:c.deleteBy,clearObjects:c.clearObjects,browseObjects:c.browseObjects,getObjectPosition:c.getObjectPosition,findObject:c.findObject,exists:c.exists,saveSynonym:c.saveSynonym,saveSynonyms:c.saveSynonyms,getSynonym:c.getSynonym,searchSynonyms:c.searchSynonyms,browseSynonyms:c.browseSynonyms,deleteSynonym:c.deleteSynonym,clearSynonyms:c.clearSynonyms,replaceAllObjects:c.replaceAllObjects,replaceAllSynonyms:c.replaceAllSynonyms,searchRules:c.searchRules,getRule:c.getRule,deleteRule:c.deleteRule,saveRule:c.saveRule,saveRules:c.saveRules,replaceAllRules:c.replaceAllRules,browseRules:c.browseRules,clearRules:c.clearRules}}),initAnalytics:()=>n=>W.createAnalyticsClient(g(u(u({},s),n),{methods:{addABTest:W.addABTest,getABTest:W.getABTest,getABTests:W.getABTests,stopABTest:W.stopABTest,deleteABTest:W.deleteABTest}})),initRecommendation:()=>n=>Pe.createRecommendationClient(g(u(u({},s),n),{methods:{getPersonalizationStrategy:Pe.getPersonalizationStrategy,setPersonalizationStrategy:Pe.setPersonalizationStrategy}}))}}))}wt.version=be.version;Tt.exports=wt});var Ut=I((mn,je)=>{var Ct=kt();je.exports=Ct;je.exports.default=Ct});var Ws={};Vt(Ws,{default:()=>Ks});var Oe=C(require("@yarnpkg/core")),E=C(require("@yarnpkg/core")),Ie=C(require("@yarnpkg/plugin-essentials")),Ht=C(require("semver"));var se=C(require("@yarnpkg/core")),Nt=C(Ut()),Cs="e8e1bd300d860104bb8c58453ffa1eb4",Us="OFCNCOG2CU",Wt=async(e,t)=>{var a;let r=se.structUtils.stringifyIdent(e),n=Ns(t).initIndex("npm-search");try{return((a=(await n.getObject(r,{attributesToRetrieve:["types"]})).types)==null?void 0:a.ts)==="definitely-typed"}catch(o){return!1}},Ns=e=>(0,Nt.default)(Us,Cs,{requester:{async send(r){try{let s=await se.httpUtils.request(r.url,r.data||null,{configuration:e,headers:r.headers});return{content:s.body,isTimedOut:!1,status:s.statusCode}}catch(s){return{content:s.response.body,isTimedOut:!1,status:s.response.statusCode}}}}});var _t=e=>e.scope?`${e.scope}__${e.name}`:`${e.name}`,Hs=async(e,t,r,s)=>{if(r.scope==="types")return;let{project:n}=e,{configuration:a}=n,o=a.makeResolver(),d={project:n,resolver:o,report:new E.ThrowReport};if(!await Wt(r,a))return;let b=_t(r),f=E.structUtils.parseRange(r.range).selector;if(!E.semverUtils.validRange(f)){let P=await o.getCandidates(r,new Map,d);f=E.structUtils.parseRange(P[0].reference).selector}let p=Ht.default.coerce(f);if(p===null)return;let h=`${Ie.suggestUtils.Modifier.CARET}${p.major}`,S=E.structUtils.makeDescriptor(E.structUtils.makeIdent("types",b),h),O=E.miscUtils.mapAndFind(n.workspaces,P=>{var T,V;let x=(T=P.manifest.dependencies.get(r.identHash))==null?void 0:T.descriptorHash,v=(V=P.manifest.devDependencies.get(r.identHash))==null?void 0:V.descriptorHash;if(x!==r.descriptorHash&&v!==r.descriptorHash)return E.miscUtils.mapAndFind.skip;let j=[];for(let Ae of Oe.Manifest.allDependencies){let Se=P.manifest[Ae].get(S.identHash);typeof Se!="undefined"&&j.push([Ae,Se])}return j.length===0?E.miscUtils.mapAndFind.skip:j});if(typeof O!="undefined")for(let[P,x]of O)e.manifest[P].set(x.identHash,x);else{try{if((await o.getCandidates(S,new Map,d)).length===0)return}catch{return}e.manifest[Ie.suggestUtils.Target.DEVELOPMENT].set(S.identHash,S)}},_s=async(e,t,r)=>{if(r.scope==="types")return;let s=_t(r),n=E.structUtils.makeIdent("types",s);for(let a of Oe.Manifest.allDependencies)typeof e.manifest[a].get(n.identHash)!="undefined"&&e.manifest[a].delete(n.identHash)},Fs=(e,t)=>{t.publishConfig&&t.publishConfig.typings&&(t.typings=t.publishConfig.typings),t.publishConfig&&t.publishConfig.types&&(t.types=t.publishConfig.types)},Bs={hooks:{afterWorkspaceDependencyAddition:Hs,afterWorkspaceDependencyRemoval:_s,beforeWorkspacePacking:Fs}},Ks=Bs;return Ws;})(); 7 | return plugin; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | 3 | nodeLinker: node-modules 4 | 5 | plugins: 6 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 7 | spec: '@yarnpkg/plugin-typescript' 8 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 9 | spec: '@yarnpkg/plugin-interactive-tools' 10 | 11 | yarnPath: .yarn/releases/yarn-3.2.0.cjs 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.0.0](https://github.com/skyra-project/audio/compare/v1.2.1...v2.0.0) (2021-09-22) 6 | 7 | ### ⚠ BREAKING CHANGES 8 | 9 | - Updated discord-api-types version from API v6 to API v9 10 | 11 | ### Features 12 | 13 | - update discord-api-types from API v6 to API v9 ([#39](https://github.com/skyra-project/audio/issues/39)) ([640e33c](https://github.com/skyra-project/audio/commit/640e33c06a4aff93e426d49578a78145ebc100f7)) 14 | 15 | ### [1.2.1](https://github.com/skyra-project/audio/compare/v1.2.0...v1.2.1) (2021-06-19) 16 | 17 | ### Bug Fixes 18 | 19 | - bump gen-esm-wrapper to fix ESM bundle ([6abd375](https://github.com/skyra-project/audio/commit/6abd37564e8d199c482104ebb304c553470b83d4)) 20 | 21 | ## [1.2.0](https://github.com/skyra-project/audio/compare/v1.1.2...v1.2.0) (2021-05-31) 22 | 23 | ### Features 24 | 25 | - **filters:** added `channelMix` and `lowPass` ([#27](https://github.com/skyra-project/audio/issues/27)) ([2ef7f7c](https://github.com/skyra-project/audio/commit/2ef7f7c26559e9424c5c8724095f35bac6cccc19)) 26 | 27 | ### [1.1.2](https://github.com/skyra-project/audio/compare/v1.1.1...v1.1.2) (2021-05-19) 28 | 29 | ### Bug Fixes 30 | 31 | - correct some types and fixed a bug ([#22](https://github.com/skyra-project/audio/issues/22)) ([9bccab5](https://github.com/skyra-project/audio/commit/9bccab5133224b939b348e0dc5d5cad0d7bd142e)) 32 | - mark package as side effect free ([cb5c824](https://github.com/skyra-project/audio/commit/cb5c82475d69890b02595ab43c193bf1899f72ed)) 33 | - **cluster:** use the correct type for ClusterNode#stats ([6623fe5](https://github.com/skyra-project/audio/commit/6623fe5fa817ad424bd86b3fbc9b574f68e3dd35)) 34 | - **docs:** prettify README example ([e9aa809](https://github.com/skyra-project/audio/commit/e9aa809a95495e464de6cc918606b42071cf7f46)) 35 | - **package:** define the correct MJS export ([1c027cc](https://github.com/skyra-project/audio/commit/1c027cc0929d357f3544d5cf105fa121f147aa12)) 36 | 37 | ### [1.1.1](https://github.com/skyra-project/audio/compare/v1.1.0...v1.1.1) (2021-03-27) 38 | 39 | ### Bug Fixes 40 | 41 | - revert regressions ([#18](https://github.com/skyra-project/audio/issues/18)) ([a3bf758](https://github.com/skyra-project/audio/commit/a3bf7585c59a815c6b3ed96d951806c9f077bd3b)) 42 | 43 | ## [1.1.0](https://github.com/skyra-project/audio/compare/v1.0.2...v1.1.0) (2021-03-27) 44 | 45 | ### Features 46 | 47 | - **filters:** added `distortion` and `rotation` ([#11](https://github.com/skyra-project/audio/issues/11)) ([5396625](https://github.com/skyra-project/audio/commit/539662515be3d67741cd08d327d6bcafba30cd69)) 48 | - prepare library for Lavalink v4 ([#10](https://github.com/skyra-project/audio/issues/10)) ([31d9edb](https://github.com/skyra-project/audio/commit/31d9edb1729817ee845ab80f22f854ebf43af6c3)) 49 | 50 | ### [1.0.2](https://github.com/skyra-project/audio/compare/v1.0.1...v1.0.2) (2020-12-22) 51 | 52 | ### [1.0.1](https://github.com/skyra-project/audio/compare/v1.0.0...v1.0.1) (2020-10-30) 53 | 54 | ### Bug Fixes 55 | 56 | - resolved Cannot read property 'OPEN' of undefined ([#3](https://github.com/skyra-project/audio/issues/3)) ([0a86435](https://github.com/skyra-project/audio/commit/0a86435754d733617684b94afa6ff1dc0078c583)) 57 | 58 | ## 1.0.0 (2020-10-19) 59 | 60 | ### Features 61 | 62 | - opt-in connect(), enhancements ([0096ff0](https://github.com/skyra-project/audio/commit/0096ff0d04907bbb75165221c293c7bbaae7587a)) 63 | 64 | ### Bug Fixes 65 | 66 | - compile as CommonJS modules ([917a3ac](https://github.com/skyra-project/audio/commit/917a3ace3e108a07c084149ff62525c8bac3f202)) 67 | - couple of bugfixes ([fb2c4ad](https://github.com/skyra-project/audio/commit/fb2c4adf0bc0168c09761ff8798fdadf36d536bf)) 68 | - couple of critical bugfixes ([717b1f5](https://github.com/skyra-project/audio/commit/717b1f55a9bb7583c80abd48de6c39b3b423dc79)) 69 | - make Connection#connect async ([1bbf719](https://github.com/skyra-project/audio/commit/1bbf719b49922654e057be0299dcc1645c524fff)) 70 | - move send out of options ([8501306](https://github.com/skyra-project/audio/commit/8501306f682200879e5b660fb1b43653fd6f07f1)) 71 | - resolved bug in audio ([18191e8](https://github.com/skyra-project/audio/commit/18191e8d78a21f89b0bd428763046005f19476e7)) 72 | - resolved bug in BaseNode#disconnect's return type ([12a033a](https://github.com/skyra-project/audio/commit/12a033a77c925b1682aebb1be785b503d396dc14)) 73 | - resolved crash, type a few more things, add more docs ([a038396](https://github.com/skyra-project/audio/commit/a038396c89ceb02ab276655b40af5261b3063b19)) 74 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © `2020` `Skyra Project` 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the “Software”), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /OTHER_LICENSES.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © `2018` `Will Nelson` 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the “Software”), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @skyra/audio 2 | 3 | [![GitHub](https://img.shields.io/github/license/skyra-project/audio)](https://github.com/skyra-project/audio/blob/main/LICENSE.md) 4 | [![codecov](https://codecov.io/gh/skyra-project/tags/branch/main/graph/badge.svg?token=WW5BOGTTKW)](https://codecov.io/gh/skyra-project/tags) 5 | [![npm](https://img.shields.io/npm/v/@skyra/audio?color=crimson&label=NPM&logo=npm&style=flat-square)](https://www.npmjs.com/package/@skyra/audio) 6 | ![npm bundle size minified (scoped)](https://img.shields.io/bundlephobia/min/@skyra/audio?label=minified&logo=webpack) 7 | 8 | **Table of Contents** 9 | 10 | - [@skyra/audio](#skyraaudio) 11 | - [About](#about) 12 | - [Installation and Usage](#installation-and-usage) 13 | - [Package managers](#package-managers) 14 | - [Usage](#usage) 15 | - [Meta](#meta) 16 | - [License](#license) 17 | - [Contributing](#contributing) 18 | - [Buy us some doughnuts](#buy-us-some-doughnuts) 19 | - [Contributors ✨](#contributors-%E2%9C%A8) 20 | 21 | ## About 22 | 23 | - A JavaScript wrapper for the [Lavalink](https://github.com/Frederikam/Lavalink) audio client for Discord. Only supports Lavalink v3. 24 | - This is a derivative work of [lavalink.js](https://github.com/lavalibs/lavalink.js), this wouldn't be possible without the author's work. 25 | 26 | ## Installation and Usage 27 | 28 | ### Package managers 29 | 30 | ```bash 31 | yarn add @skyra/audio 32 | # or npm install @skyra/audio 33 | ``` 34 | 35 | #### Usage 36 | 37 | ```js 38 | const { Node } = require('@skyra/audio'); 39 | 40 | const node = new Node( 41 | { 42 | // Your Lavalink password: 43 | password: '', 44 | // The user ID of your bot: 45 | userID: '', 46 | // The total number of shards that your bot is running (optional, useful if you're load balancing): 47 | shardCount: 0, 48 | // A URL to your lavalink instance without protocol (optional, can be used instead of specifying hosts option): 49 | host: '', 50 | // Alternatively, define a custom rest and ws host links: 51 | hosts: { 52 | // The http host of your lavalink instance (optional): 53 | rest: '', 54 | // The ws host of your lavalink instance (optional): 55 | ws: '' 56 | } 57 | }, 58 | (guildID, packet) => { 59 | const guild = client.guilds.cache.get(guildID); 60 | if (guild) return guild.shard.send(packet); 61 | } 62 | ); 63 | await node.connect(); 64 | 65 | // This sends the required raw Voice State and Voice Server data to lavalink so it can make a connection. 66 | client.ws.on('VOICE_STATE_UPDATE', async (data) => { 67 | try { 68 | await node.voiceStateUpdate(data); 69 | } catch (error) { 70 | console.error(error); 71 | } 72 | }); 73 | 74 | client.ws.on('VOICE_SERVER_UPDATE', async (data) => { 75 | try { 76 | await node.voiceServerUpdate(data); 77 | } catch (error) { 78 | console.error(error); 79 | } 80 | }); 81 | ``` 82 | 83 | ```ts 84 | import { Node } from '@skyra/audio'; 85 | 86 | // Same as before 87 | ``` 88 | 89 | ## Meta 90 | 91 | ### License 92 | 93 | Copyright © 2020, [Skyra Project](https://github.com/skyra-project). 94 | Released under the [MIT License](LICENSE.md). 95 | 96 | ### Contributing 97 | 98 | 1. Fork it! 99 | 1. Create your feature branch: `git checkout -b my-new-feature` 100 | 1. Commit your changes: `git commit -am 'Add some feature'` 101 | 1. Push to the branch: `git push origin my-new-feature` 102 | 1. Submit a pull request! 103 | 104 | ### Buy us some doughnuts 105 | 106 | Skyra Project is open source and always will be, even if we don't get donations. That said, we know there are amazing people who 107 | may still want to donate just to show their appreciation. Thanks you very much in advance! 108 | 109 | We accept donations through Patreon, BitCoin, Ethereum, and Litecoin. You can use the buttons below to donate through your method of choice. 110 | 111 | | Donate With | QR | Address | 112 | | :---------: | :----------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | 113 | | Patreon | ![PatreonImage][] | [Click Here](https://donate.skyra.pw/patreon) | 114 | | PayPal | ![PayPalImage][] | [Click Here](https://donate.skyra.pw/paypal) | 115 | | BitCoin | ![BitcoinImage][] | [3JNzCHMTFtxYFWBnVtDM9Tt34zFbKvdwco](bitcoin:3JNzCHMTFtxYFWBnVtDM9Tt34zFbKvdwco?amount=0.01&label=Skyra%20Discord%20Bot) | 116 | | Ethereum | ![EthereumImage][] | [0xcB5EDB76Bc9E389514F905D9680589004C00190c](ethereum:0xcB5EDB76Bc9E389514F905D9680589004C00190c?amount=0.01&label=Skyra%20Discord%20Bot) | 117 | | Litecoin | ![LitecoinImage][] | [MNVT1keYGMfGp7vWmcYjCS8ntU8LNvjnqM](litecoin:MNVT1keYGMfGp7vWmcYjCS8ntU8LNvjnqM?amount=0.01&label=Skyra%20Discord%20Bot) | 118 | 119 | [patreonimage]: https://cdn.skyra.pw/gh-assets/patreon.png 120 | [paypalimage]: https://cdn.skyra.pw/gh-assets/paypal.png 121 | [bitcoinimage]: https://cdn.skyra.pw/gh-assets/bitcoin.png 122 | [ethereumimage]: https://cdn.skyra.pw/gh-assets/ethereum.png 123 | [litecoinimage]: https://cdn.skyra.pw/gh-assets/litecoin.png 124 | 125 | ### Contributors ✨ 126 | 127 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 |

Antonio Román

💻 ⚠️ 🤔 🚇

Jeroen Claassens

💻 🚇 🚧
138 | 139 | 140 | 141 | 142 | 143 | 144 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 145 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/require-await 4 | export default async (): Promise => ({ 5 | coverageProvider: 'v8', 6 | displayName: 'unit test', 7 | preset: 'ts-jest', 8 | testEnvironment: 'node', 9 | testRunner: 'jest-circus/runner', 10 | testMatch: ['/tests/**/*.test.ts'], 11 | globals: { 12 | 'ts-jest': { 13 | tsconfig: '/tests/tsconfig.json' 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@skyra/audio", 3 | "version": "2.0.0", 4 | "description": "A TypeScript wrapper for Lavalink.", 5 | "author": "@skyra", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "module": "dist/index.mjs", 9 | "types": "dist/index.d.ts", 10 | "exports": { 11 | "require": "./dist/index.js", 12 | "import": "./dist/index.mjs" 13 | }, 14 | "sideEffects": "false", 15 | "homepage": "https://skyra-project.github.io/audio", 16 | "files": [ 17 | "dist", 18 | "!dist/*.tsbuildinfo" 19 | ], 20 | "scripts": { 21 | "lint": "eslint src tests --ext ts --fix", 22 | "format": "prettier --write \"{src,tests}/**/*.ts\"", 23 | "docs": "typedoc", 24 | "test": "jest", 25 | "test:watch": "jest --watch", 26 | "update": "yarn up \"@*/*\" -i && yarn up \"*\" -i", 27 | "clean": "node scripts/clean.mjs", 28 | "build": "tsc -b src && gen-esm-wrapper dist/index.js dist/index.mjs", 29 | "watch": "tsc -b src -w", 30 | "sversion": "standard-version", 31 | "prepublishOnly": "yarn build", 32 | "prepare": "husky install .github/husky" 33 | }, 34 | "dependencies": { 35 | "backoff": "^2.5.0", 36 | "tslib": "^2.3.1", 37 | "ws": "^8.5.0" 38 | }, 39 | "devDependencies": { 40 | "@commitlint/cli": "^16.2.3", 41 | "@commitlint/config-conventional": "^16.2.1", 42 | "@sapphire/eslint-config": "^4.3.3", 43 | "@sapphire/prettier-config": "^1.4.2", 44 | "@sapphire/ts-config": "^3.3.4", 45 | "@types/backoff": "^2.5.2", 46 | "@types/jest": "^27.4.1", 47 | "@types/node": "^17.0.8", 48 | "@types/ws": "^8.5.3", 49 | "@typescript-eslint/eslint-plugin": "^5.17.0", 50 | "@typescript-eslint/parser": "^5.17.0", 51 | "cz-conventional-changelog": "^3.3.0", 52 | "discord-api-types": "^0.30.0", 53 | "eslint": "^8.12.0", 54 | "eslint-config-prettier": "^8.5.0", 55 | "eslint-plugin-prettier": "^4.0.0", 56 | "gen-esm-wrapper": "^1.1.3", 57 | "husky": "^7.0.4", 58 | "jest": "^27.5.1", 59 | "jest-circus": "^27.5.1", 60 | "lint-staged": "^12.3.7", 61 | "prettier": "^2.6.2", 62 | "pretty-quick": "^3.1.3", 63 | "standard-version": "^9.3.2", 64 | "ts-jest": "^27.1.4", 65 | "ts-node": "^10.7.0", 66 | "typedoc": "^0.22.13", 67 | "typescript": "^4.6.3" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "git+https://github.com/skyra-project/audio.git" 72 | }, 73 | "engines": { 74 | "node": ">=v14.18.0", 75 | "npm": ">=7.24.2" 76 | }, 77 | "keywords": [ 78 | "audio", 79 | "lavalink", 80 | "typescript", 81 | "ts", 82 | "utility" 83 | ], 84 | "bugs": { 85 | "url": "https://github.com/skyra-project/audio/issues" 86 | }, 87 | "commitlint": { 88 | "extends": [ 89 | "@commitlint/config-conventional" 90 | ] 91 | }, 92 | "lint-staged": { 93 | "*.{mjs,js,ts}": "eslint --fix --ext mjs,js,ts" 94 | }, 95 | "config": { 96 | "commitizen": { 97 | "path": "./node_modules/cz-conventional-changelog" 98 | } 99 | }, 100 | "publishConfig": { 101 | "access": "public" 102 | }, 103 | "prettier": "@sapphire/prettier-config", 104 | "packageManager": "yarn@3.2.0" 105 | } 106 | -------------------------------------------------------------------------------- /scripts/clean.mjs: -------------------------------------------------------------------------------- 1 | import { rm } from 'node:fs/promises'; 2 | 3 | const distFolder = new URL('../dist/', import.meta.url); 4 | 5 | await rm(distFolder, { recursive: true, force: true }); 6 | -------------------------------------------------------------------------------- /src/Cluster.ts: -------------------------------------------------------------------------------- 1 | import { BaseCluster, ClusterFilter, ClusterSend } from './base/BaseCluster'; 2 | import type { ClusterNodeOptions } from './ClusterNode'; 3 | 4 | export interface ClusterOptions { 5 | filter?: ClusterFilter; 6 | nodes?: ClusterNodeOptions[]; 7 | } 8 | 9 | export class Cluster extends BaseCluster { 10 | public filter: ClusterFilter; 11 | public send: ClusterSend; 12 | 13 | public constructor(options: ClusterOptions, send: ClusterSend) { 14 | super(options.nodes); 15 | this.filter = options.filter || (() => true); 16 | this.send = send; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ClusterNode.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage } from 'http'; 2 | import type { IncomingEventPayload, IncomingPlayerUpdatePayload, IncomingStatsPayload } from './types/IncomingPayloads'; 3 | import type { BaseCluster, ClusterSend } from './base/BaseCluster'; 4 | import { BaseNode, NodeOptions } from './base/BaseNode'; 5 | import { ConnectionEvents } from './core/Connection'; 6 | 7 | export interface ClusterNodeOptions extends NodeOptions { 8 | tags?: Iterable; 9 | } 10 | 11 | export type Stats = Omit; 12 | 13 | export class ClusterNode extends BaseNode { 14 | public tags: Set; 15 | public send: ClusterSend; 16 | public stats: Stats | null; 17 | 18 | public constructor(public readonly cluster: BaseCluster, options: ClusterNodeOptions) { 19 | super(options); 20 | this.tags = new Set(options.tags || []); 21 | this.send = this.cluster.send.bind(this.cluster); 22 | this.stats = null; 23 | 24 | this.on(ConnectionEvents.Stats, (stats) => (this.stats = stats)); 25 | } 26 | 27 | public emit(event: ConnectionEvents.Close, code: number, reason: string): boolean; 28 | public emit(event: ConnectionEvents.Error, error: Error): boolean; 29 | public emit(event: ConnectionEvents.Event, payload: IncomingEventPayload): boolean; 30 | public emit(event: ConnectionEvents.Open): boolean; 31 | public emit(event: ConnectionEvents.PlayerUpdate, payload: IncomingPlayerUpdatePayload): boolean; 32 | public emit(event: ConnectionEvents.Stats, payload: IncomingStatsPayload): boolean; 33 | public emit(event: ConnectionEvents.Upgrade, req: IncomingMessage): boolean; 34 | public emit(event: ConnectionEvents, ...args: readonly any[]): boolean { 35 | // @ts-expect-error Expect same arguments as parent. 36 | if (this.listenerCount(event)) super.emit(event, ...args); 37 | return this.cluster.emit(event, ...args); 38 | } 39 | 40 | public async destroy(code?: number, data?: string): Promise { 41 | await super.destroy(code, data); 42 | this.cluster.nodes.splice(this.cluster.nodes.indexOf(this), 1); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Node.ts: -------------------------------------------------------------------------------- 1 | import { BaseNode, NodeOptions, NodeSend } from './base/BaseNode'; 2 | 3 | export class Node extends BaseNode { 4 | public send: NodeSend; 5 | 6 | public constructor(options: NodeOptions, send: NodeSend) { 7 | super(options); 8 | this.send = send; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/base/BaseCluster.ts: -------------------------------------------------------------------------------- 1 | import type { GatewaySendPayload } from 'discord-api-types/v9'; 2 | import { EventEmitter } from 'events'; 3 | import { ClusterNode, ClusterNodeOptions } from '../ClusterNode'; 4 | import type { Player } from '../core/Player'; 5 | import type { VoiceServerUpdate, VoiceStateUpdate } from './BaseNode'; 6 | 7 | export interface ClusterFilter { 8 | (node: ClusterNode, guildID: string): boolean; 9 | } 10 | 11 | export interface ClusterSend { 12 | (guildID: string, packet: GatewaySendPayload): unknown; 13 | } 14 | 15 | export abstract class BaseCluster extends EventEmitter { 16 | public abstract send: ClusterSend; 17 | public abstract filter: ClusterFilter; 18 | 19 | public readonly nodes: ClusterNode[] = []; 20 | 21 | public constructor(options?: ClusterNodeOptions[]) { 22 | super(); 23 | if (options) this.spawn(options); 24 | } 25 | 26 | public connect() { 27 | return Promise.all(this.nodes.map((node) => node.connect())); 28 | } 29 | 30 | public spawn(options: ClusterNodeOptions): ClusterNode; 31 | public spawn(options: ClusterNodeOptions[]): ClusterNode[]; 32 | public spawn(options: ClusterNodeOptions | ClusterNodeOptions[]): ClusterNode | ClusterNode[] { 33 | if (Array.isArray(options)) return options.map((opt) => this.spawn(opt)); 34 | 35 | const node = new ClusterNode(this, options); 36 | this.nodes.push(node); 37 | return node; 38 | } 39 | 40 | public sort(): ClusterNode[] { 41 | return this.nodes 42 | .filter((n) => n.connected) 43 | .sort((a, b) => { 44 | // sort by overall system cpu load 45 | if (!a.stats || !b.stats) return -1; 46 | return ( 47 | (a.stats.cpu ? a.stats.cpu.systemLoad / a.stats.cpu.cores : 0) - (b.stats.cpu ? b.stats.cpu.systemLoad / b.stats.cpu.cores : 0) 48 | ); 49 | }); 50 | } 51 | 52 | public getNode(guildID: string): ClusterNode { 53 | let node = this.nodes.find((node) => node.players.has(guildID)); 54 | if (!node) node = this.sort().find((node) => this.filter(node, guildID)); 55 | if (node) return node; 56 | throw new Error('unable to find appropriate node; please check your filter'); 57 | } 58 | 59 | public has(guildID: string): boolean { 60 | return this.nodes.some((node) => node.players.has(guildID)); 61 | } 62 | 63 | public get(guildID: string): Player { 64 | return this.getNode(guildID).players.get(guildID); 65 | } 66 | 67 | public voiceStateUpdate(state: VoiceStateUpdate): Promise { 68 | return this.getNode(state.guild_id!).voiceStateUpdate(state); 69 | } 70 | 71 | public voiceServerUpdate(server: VoiceServerUpdate): Promise { 72 | return this.getNode(server.guild_id).voiceServerUpdate(server); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/base/BaseNode.ts: -------------------------------------------------------------------------------- 1 | import type { GatewaySendPayload, GatewayVoiceServerUpdateDispatch, GatewayVoiceState } from 'discord-api-types/v9'; 2 | import { EventEmitter } from 'events'; 3 | import type { IncomingMessage } from 'http'; 4 | import * as WebSocket from 'ws'; 5 | import { Connection, ConnectionEvents, Options as ConnectionOptions } from '../core/Connection'; 6 | import { Http, Track, TrackInfo, TrackResponse } from '../core/Http'; 7 | import { PlayerStore } from '../core/PlayerStore'; 8 | import type { IncomingEventPayload, IncomingPlayerUpdatePayload, IncomingStatsPayload } from '../types/IncomingPayloads'; 9 | 10 | export type VoiceServerUpdate = GatewayVoiceServerUpdateDispatch['d']; 11 | export type VoiceStateUpdate = GatewayVoiceState; 12 | 13 | /** 14 | * The options for the node. 15 | */ 16 | export interface NodeOptions { 17 | /** 18 | * The password to use to login to the Lavalink server. 19 | * @example 20 | * ```json 21 | * "you-shall-not-pass" 22 | * ``` 23 | */ 24 | password: string; 25 | 26 | /** 27 | * The client's user ID. 28 | * @example 29 | * ```json 30 | * "266624760782258186" 31 | * ``` 32 | */ 33 | userID: string; 34 | 35 | /** 36 | * The total number of shards that your bot is running. (Optional, useful if you are load balancing). 37 | * @example 38 | * ```json 39 | * 0 40 | * ``` 41 | */ 42 | shardCount?: number; 43 | 44 | /** 45 | * The host options to use, this is a more advanced alternative to [[BaseNodeOptions.host]]. 46 | */ 47 | hosts?: { 48 | /** 49 | * The HTTP host of your Lavalink instance. 50 | * @example 51 | * ```json 52 | * "http://localhost" 53 | * ``` 54 | * 55 | * @example 56 | * ```json 57 | * "http://localhost:2333" 58 | * ``` 59 | */ 60 | rest?: string; 61 | 62 | /** 63 | * The WS host of your Lavalink instance. 64 | * @example 65 | * ```json 66 | * "ws://localhost" 67 | * ``` 68 | * 69 | * @example 70 | * ```json 71 | * "ws://localhost:2333" 72 | * ``` 73 | */ 74 | ws?: string | { url: string; options: ConnectionOptions }; 75 | }; 76 | 77 | /** 78 | * A URL to your Lavalink instance without protocol. 79 | * @example 80 | * ```json 81 | * "localhost" 82 | * ``` 83 | * 84 | * @example 85 | * ```json 86 | * "localhost:2333" 87 | * ``` 88 | */ 89 | host?: string; 90 | } 91 | 92 | export interface NodeSend { 93 | (guildID: string, packet: GatewaySendPayload): unknown; 94 | } 95 | 96 | export abstract class BaseNode extends EventEmitter { 97 | public abstract send: NodeSend; 98 | 99 | public password: string; 100 | public userID: string; 101 | public shardCount?: number; 102 | 103 | public players: PlayerStore; 104 | public http: Http | null = null; 105 | public connection: Connection | null = null; 106 | 107 | public voiceStates = new Map(); 108 | public voiceServers = new Map(); 109 | 110 | private _expectingConnection = new Set(); 111 | 112 | public constructor({ password, userID, shardCount, hosts, host }: NodeOptions) { 113 | super(); 114 | this.password = password; 115 | this.userID = userID; 116 | this.shardCount = shardCount; 117 | this.players = new PlayerStore(this); 118 | 119 | if (host) { 120 | this.http = new Http(this, `http://${host}`); 121 | this.connection = new Connection(this, `ws://${host}`); 122 | } else if (hosts) { 123 | this.http = hosts.rest ? new Http(this, hosts.rest) : null; 124 | this.connection = hosts.ws 125 | ? typeof hosts.ws === 'string' 126 | ? new Connection(this, hosts.ws) 127 | : new Connection(this, hosts.ws.url, hosts.ws.options) 128 | : null; 129 | } 130 | } 131 | 132 | /** 133 | * Connects to the server. 134 | */ 135 | public connect(): Promise { 136 | return this.connection!.connect(); 137 | } 138 | 139 | /** 140 | * Whether or not the node is connected to the websocket. 141 | */ 142 | public get connected(): boolean { 143 | return this.connection?.ws?.readyState === WebSocket.OPEN; 144 | } 145 | 146 | /** 147 | * Loads a song. 148 | * @param identifier The track to be loaded. 149 | * @example 150 | * ```typescript 151 | * // Load from URL: 152 | * 153 | * const result = await node.load('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); 154 | * console.log(result); 155 | * // { 156 | * // "loadType": "TRACK_LOADED", 157 | * // "playlistInfo": {}, 158 | * // "tracks": [ 159 | * // { 160 | * // "track": "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==", 161 | * // "info": { 162 | * // "identifier": "dQw4w9WgXcQ", 163 | * // "isSeekable": true, 164 | * // "author": "RickAstleyVEVO", 165 | * // "length": 212000, 166 | * // "isStream": false, 167 | * // "position": 0, 168 | * // "title": "Rick Astley - Never Gonna Give You Up", 169 | * // "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 170 | * // } 171 | * // } 172 | * // ] 173 | * // } 174 | * ``` 175 | * 176 | * @example 177 | * ```typescript 178 | * // Load from YouTube search: 179 | * 180 | * const result = await node.load('ytsearch: Never Gonna Give You Up'); 181 | * console.log(result); 182 | * // { 183 | * // "loadType": "SEARCH_RESULT", 184 | * // "playlistInfo": {}, 185 | * // "tracks": [ 186 | * // { 187 | * // "track": "...", 188 | * // "info": { 189 | * // "identifier": "dQw4w9WgXcQ", 190 | * // "isSeekable": true, 191 | * // "author": "RickAstleyVEVO", 192 | * // "length": 212000, 193 | * // "isStream": false, 194 | * // "position": 0, 195 | * // "title": "Rick Astley - Never Gonna Give You Up", 196 | * // "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 197 | * // } 198 | * // }, 199 | * // ... 200 | * // ] 201 | * // } 202 | * ``` 203 | */ 204 | public load(identifier: string): Promise { 205 | if (this.http) return this.http.load(identifier); 206 | throw new Error('no available http module'); 207 | } 208 | 209 | /** 210 | * Decodes a track. 211 | * @param track The track to be decoded. 212 | * @example 213 | * ```typescript 214 | * const identifier = 'QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA=='; 215 | * 216 | * const track = await http.decode(identifier); 217 | * console.log(track); 218 | * // Logs: { 219 | * // "identifier": "dQw4w9WgXcQ", 220 | * // "isSeekable": true, 221 | * // "author": "RickAstleyVEVO", 222 | * // "length": 212000, 223 | * // "isStream": false, 224 | * // "position": 0, 225 | * // "title": "Rick Astley - Never Gonna Give You Up", 226 | * // "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 227 | * // } 228 | * ``` 229 | */ 230 | public decode(track: string): Promise; 231 | 232 | /** 233 | * Decodes multiple tracks. 234 | * @note This returns an array of [[Track]]s, not a [[TrackInfo]]. 235 | * @param tracks The tracks to be decoded. 236 | */ 237 | public decode(tracks: string[]): Promise; 238 | public decode(tracks: string | string[]): Promise { 239 | if (this.http) return this.http.decode(tracks); 240 | throw new Error('no available http module'); 241 | } 242 | 243 | public voiceStateUpdate(packet: VoiceStateUpdate): Promise { 244 | if (packet.user_id !== this.userID) return Promise.resolve(false); 245 | 246 | if (packet.channel_id) { 247 | this.voiceStates.set(packet.guild_id!, packet); 248 | return this._tryConnection(packet.guild_id!); 249 | } 250 | 251 | this.voiceServers.delete(packet.guild_id!); 252 | this.voiceStates.delete(packet.guild_id!); 253 | 254 | return Promise.resolve(false); 255 | } 256 | 257 | public voiceServerUpdate(packet: VoiceServerUpdate): Promise { 258 | this.voiceServers.set(packet.guild_id!, packet); 259 | this._expectingConnection.add(packet.guild_id!); 260 | return this._tryConnection(packet.guild_id!); 261 | } 262 | 263 | public disconnect(code?: number, data?: string): Promise { 264 | if (this.connection) return this.connection.close(code, data); 265 | return Promise.resolve(false); 266 | } 267 | 268 | public async destroy(code?: number, data?: string): Promise { 269 | await Promise.all([...this.players.values()].map((player) => player.destroy())); 270 | await this.disconnect(code, data); 271 | } 272 | 273 | private async _tryConnection(guildID: string): Promise { 274 | const state = this.voiceStates.get(guildID); 275 | const server = this.voiceServers.get(guildID); 276 | if (!state || !server || !this._expectingConnection.has(guildID)) return false; 277 | 278 | await this.players.get(guildID).voiceUpdate(state.session_id, server); 279 | this._expectingConnection.delete(guildID); 280 | return true; 281 | } 282 | } 283 | 284 | /* eslint-disable @typescript-eslint/unified-signatures */ 285 | export interface BaseNode { 286 | on(event: ConnectionEvents.Close, cb: (code: number, reason: string) => void): this; 287 | on(event: ConnectionEvents.Error, cb: (error: Error) => void): this; 288 | on(event: ConnectionEvents.Event, cb: (payload: IncomingEventPayload) => void): this; 289 | on(event: ConnectionEvents.Open, cb: () => void): this; 290 | on(event: ConnectionEvents.PlayerUpdate, cb: (payload: IncomingPlayerUpdatePayload) => void): this; 291 | on(event: ConnectionEvents.Stats, cb: (payload: IncomingStatsPayload) => void): this; 292 | on(event: ConnectionEvents.Upgrade, cb: (req: IncomingMessage) => void): this; 293 | 294 | once(event: ConnectionEvents.Close, cb: (code: number, reason: string) => void): this; 295 | once(event: ConnectionEvents.Error, cb: (error: Error) => void): this; 296 | once(event: ConnectionEvents.Event, cb: (payload: IncomingEventPayload) => void): this; 297 | once(event: ConnectionEvents.Open, cb: () => void): this; 298 | once(event: ConnectionEvents.PlayerUpdate, cb: (payload: IncomingPlayerUpdatePayload) => void): this; 299 | once(event: ConnectionEvents.Stats, cb: (payload: IncomingStatsPayload) => void): this; 300 | once(event: ConnectionEvents.Upgrade, cb: (req: IncomingMessage) => void): this; 301 | 302 | addListener(event: ConnectionEvents.Close, cb: (code: number, reason: string) => void): this; 303 | addListener(event: ConnectionEvents.Error, cb: (error: Error) => void): this; 304 | addListener(event: ConnectionEvents.Event, cb: (payload: IncomingEventPayload) => void): this; 305 | addListener(event: ConnectionEvents.Open, cb: () => void): this; 306 | addListener(event: ConnectionEvents.PlayerUpdate, cb: (payload: IncomingPlayerUpdatePayload) => void): this; 307 | addListener(event: ConnectionEvents.Stats, cb: (payload: IncomingStatsPayload) => void): this; 308 | addListener(event: ConnectionEvents.Upgrade, cb: (req: IncomingMessage) => void): this; 309 | 310 | off(event: ConnectionEvents.Close, cb: (code: number, reason: string) => void): this; 311 | off(event: ConnectionEvents.Error, cb: (error: Error) => void): this; 312 | off(event: ConnectionEvents.Event, cb: (payload: IncomingEventPayload) => void): this; 313 | off(event: ConnectionEvents.Open, cb: () => void): this; 314 | off(event: ConnectionEvents.PlayerUpdate, cb: (payload: IncomingPlayerUpdatePayload) => void): this; 315 | off(event: ConnectionEvents.Stats, cb: (payload: IncomingStatsPayload) => void): this; 316 | off(event: ConnectionEvents.Upgrade, cb: (req: IncomingMessage) => void): this; 317 | 318 | removeListener(event: ConnectionEvents.Close, cb: (code: number, reason: string) => void): this; 319 | removeListener(event: ConnectionEvents.Error, cb: (error: Error) => void): this; 320 | removeListener(event: ConnectionEvents.Event, cb: (payload: IncomingEventPayload) => void): this; 321 | removeListener(event: ConnectionEvents.Open, cb: () => void): this; 322 | removeListener(event: ConnectionEvents.PlayerUpdate, cb: (payload: IncomingPlayerUpdatePayload) => void): this; 323 | removeListener(event: ConnectionEvents.Stats, cb: (payload: IncomingStatsPayload) => void): this; 324 | removeListener(event: ConnectionEvents.Upgrade, cb: (req: IncomingMessage) => void): this; 325 | 326 | emit(event: ConnectionEvents.Close, code: number, reason: string): boolean; 327 | emit(event: ConnectionEvents.Error, error: Error): boolean; 328 | emit(event: ConnectionEvents.Event, payload: IncomingEventPayload): boolean; 329 | emit(event: ConnectionEvents.Open): boolean; 330 | emit(event: ConnectionEvents.PlayerUpdate, payload: IncomingPlayerUpdatePayload): boolean; 331 | emit(event: ConnectionEvents.Stats, payload: IncomingStatsPayload): boolean; 332 | emit(event: ConnectionEvents.Upgrade, req: IncomingMessage): boolean; 333 | } 334 | /* eslint-enable @typescript-eslint/unified-signatures */ 335 | -------------------------------------------------------------------------------- /src/core/Connection.ts: -------------------------------------------------------------------------------- 1 | import { Backoff, exponential } from 'backoff'; 2 | import { once } from 'events'; 3 | import type { IncomingMessage } from 'http'; 4 | import WebSocket from 'ws'; 5 | import type { BaseNode } from '../base/BaseNode'; 6 | import type { IncomingPayload } from '../types/IncomingPayloads'; 7 | import type { OutgoingPayload } from '../types/OutgoingPayloads'; 8 | 9 | interface Sendable { 10 | resolve: () => void; 11 | reject: (e: Error) => void; 12 | data: Buffer | string; 13 | } 14 | 15 | interface Headers { 16 | Authorization: string; 17 | 'Num-Shards': number; 18 | 'User-Id': string; 19 | 'Client-Name': string; 20 | 'Resume-Key'?: string; 21 | } 22 | 23 | export const enum WebSocketEvents { 24 | Open = 'open', 25 | Close = 'close', 26 | Upgrade = 'upgrade', 27 | Message = 'message', 28 | Error = 'error' 29 | } 30 | 31 | export const enum ConnectionEvents { 32 | Close = 'close', 33 | Error = 'error', 34 | Event = 'event', 35 | Open = 'open', 36 | PlayerUpdate = 'playerUpdate', 37 | Stats = 'stats', 38 | Upgrade = 'upgrade' 39 | } 40 | 41 | export interface Options extends WebSocket.ClientOptions { 42 | /** 43 | * The key to send when resuming the session. Set to `null` or leave unset to disable resuming. 44 | * @defaults `null` 45 | */ 46 | resumeKey?: string | null; 47 | 48 | /** 49 | * The number of seconds after disconnecting before the session is closed anyways. This is useful for avoiding 50 | * accidental leaks. 51 | * @defaults `60` 52 | */ 53 | resumeTimeout?: number; 54 | } 55 | 56 | export class Connection { 57 | /** 58 | * The node that owns this connection. 59 | */ 60 | public readonly node: T; 61 | 62 | /** 63 | * The url the connection connects to. 64 | */ 65 | public readonly url: string; 66 | 67 | /** 68 | * The websocket options. 69 | */ 70 | public readonly options: Options; 71 | 72 | /** 73 | * The resume key, check [[Options.resumeKey]] for more information. 74 | */ 75 | public resumeKey?: string | null; 76 | 77 | /** 78 | * The websocket connection. 79 | */ 80 | public ws: WebSocket | null; 81 | 82 | /** 83 | * The back-off queue. 84 | */ 85 | #backoff: Backoff = exponential(); 86 | 87 | /** 88 | * The queue of requests to be processed. 89 | */ 90 | #queue: Sendable[] = []; 91 | 92 | /** 93 | * The bound callback function for `wsSend`. 94 | */ 95 | #send: Connection['wsSend']; 96 | 97 | /** 98 | * The bound callback function for `onOpen`. 99 | */ 100 | #open: Connection['onOpen']; 101 | 102 | /** 103 | * The bound callback function for `onClose`. 104 | */ 105 | #close: Connection['onClose']; 106 | 107 | /** 108 | * The bound callback function for `onUpgrade`. 109 | */ 110 | #upgrade: Connection['onUpgrade']; 111 | 112 | /** 113 | * The bound callback function for `onMessage`. 114 | */ 115 | #message: Connection['onMessage']; 116 | 117 | /** 118 | * The bound callback function for `onError`. 119 | */ 120 | #error: Connection['onError']; 121 | 122 | public constructor(node: T, url: string, options: Options = {}) { 123 | this.node = node; 124 | this.url = url; 125 | this.options = options; 126 | this.resumeKey = options.resumeKey; 127 | 128 | this.ws = null; 129 | this.#send = this.wsSend.bind(this); 130 | this.#open = this.onOpen.bind(this); 131 | this.#close = this.onClose.bind(this); 132 | this.#upgrade = this.onUpgrade.bind(this); 133 | this.#message = this.onMessage.bind(this); 134 | this.#error = this.onError.bind(this); 135 | } 136 | 137 | /** 138 | * Connects to the server. 139 | */ 140 | public connect(): Promise { 141 | // Create a new ready listener if none was set. 142 | if (!this.#backoff.listenerCount('ready')) { 143 | this.#backoff.on('ready', () => this._connect().catch((error) => this.node.emit(ConnectionEvents.Error, error))); 144 | } 145 | 146 | return this._connect(); 147 | } 148 | 149 | /** 150 | * Configures the resuming for this connection. 151 | * @param timeout The number of seconds after disconnecting before the session is closed anyways. 152 | * This is useful for avoiding accidental leaks. 153 | * @param key The key to send when resuming the session. Set to `null` or leave unset to disable resuming. 154 | */ 155 | public configureResuming(timeout = 60, key: string | null = null): Promise { 156 | this.resumeKey = key; 157 | 158 | return this.send({ 159 | op: 'configureResuming', 160 | key, 161 | timeout 162 | }); 163 | } 164 | 165 | /** 166 | * Sends a message to the websocket. 167 | * @param payload The data to be sent to the websocket. 168 | */ 169 | public send(payload: OutgoingPayload): Promise { 170 | if (!this.ws) return Promise.reject(new Error('The client has not been initialized.')); 171 | 172 | return new Promise((resolve, reject) => { 173 | const encoded = JSON.stringify(payload); 174 | const send = { resolve, reject, data: encoded }; 175 | 176 | if (this.ws!.readyState === WebSocket.OPEN) this.wsSend(send); 177 | else this.#queue.push(send); 178 | }); 179 | } 180 | 181 | /** 182 | * Closes the WebSocket connection. 183 | * @param code The close code. 184 | * @param data The data to be sent. 185 | */ 186 | public async close(code?: number, data?: string): Promise { 187 | if (!this.ws) return false; 188 | 189 | this.ws.off(WebSocketEvents.Close, this.#close); 190 | 191 | this.ws!.close(code, data); 192 | 193 | // @ts-expect-error Arguments are passed, TypeScript just does not recognize them. 194 | this.node.emit(ConnectionEvents.Close, ...(await once(this.ws, WebSocketEvents.Close))); 195 | this.#backoff.removeAllListeners(); 196 | this.ws!.removeAllListeners(); 197 | this.ws = null; 198 | 199 | return true; 200 | } 201 | 202 | private async _connect() { 203 | if (this.ws?.readyState === WebSocket.OPEN) { 204 | this.ws.close(); 205 | this.ws.removeAllListeners(); 206 | 207 | // @ts-expect-error Arguments are passed, TypeScript just does not recognize them. 208 | this.node.emit(ConnectionEvents.Close, ...(await once(this.ws, WebSocketEvents.Close))); 209 | } 210 | 211 | const headers: Headers = { 212 | Authorization: this.node.password, 213 | 'Num-Shards': this.node.shardCount || 1, 214 | 'Client-Name': '@skyra/audio', 215 | 'User-Id': this.node.userID 216 | }; 217 | 218 | if (this.resumeKey) headers['Resume-Key'] = this.resumeKey; 219 | 220 | const ws = new WebSocket(this.url, { headers, ...this.options } as WebSocket.ClientOptions); 221 | this.ws = ws; 222 | this._registerWSEventListeners(); 223 | 224 | return new Promise((resolve, reject) => { 225 | // eslint-disable-next-line @typescript-eslint/no-this-alias 226 | const self = this; 227 | 228 | function onOpen() { 229 | resolve(); 230 | cleanup(); 231 | } 232 | 233 | function onError(error: Error) { 234 | self.ws = null; 235 | reject(error); 236 | cleanup(); 237 | } 238 | 239 | function onClose(code: number, reason: string) { 240 | self.ws = null; 241 | reject(new Error(`Closed connection with code ${code} and reason ${reason}`)); 242 | cleanup(); 243 | } 244 | 245 | function cleanup() { 246 | ws.off(WebSocketEvents.Open, onOpen); 247 | ws.off(WebSocketEvents.Error, onError); 248 | ws.off(WebSocketEvents.Close, onClose); 249 | } 250 | 251 | ws.on(WebSocketEvents.Open, onOpen); 252 | ws.on(WebSocketEvents.Error, onError); 253 | ws.on(WebSocketEvents.Close, onClose); 254 | }); 255 | } 256 | 257 | private _reconnect() { 258 | if (!this.ws || this.ws.readyState === WebSocket.CLOSED) this.#backoff.backoff(); 259 | } 260 | 261 | private _registerWSEventListeners() { 262 | if (!this.ws!.listeners(WebSocketEvents.Open).includes(this.#open)) this.ws!.on(WebSocketEvents.Open, this.#open); 263 | if (!this.ws!.listeners(WebSocketEvents.Close).includes(this.#close)) this.ws!.on(WebSocketEvents.Close, this.#close); 264 | if (!this.ws!.listeners(WebSocketEvents.Upgrade).includes(this.#upgrade)) this.ws!.on(WebSocketEvents.Upgrade, this.#upgrade); 265 | if (!this.ws!.listeners(WebSocketEvents.Message).includes(this.#message)) this.ws!.on(WebSocketEvents.Message, this.#message); 266 | if (!this.ws!.listeners(WebSocketEvents.Error).includes(this.#error)) this.ws!.on(WebSocketEvents.Error, this.#error); 267 | } 268 | 269 | private async _flush() { 270 | await Promise.all(this.#queue.map(this.#send)); 271 | this.#queue = []; 272 | } 273 | 274 | private wsSend({ resolve, reject, data }: Sendable) { 275 | this.ws!.send(data, (err) => { 276 | if (err) reject(err); 277 | else resolve(); 278 | }); 279 | } 280 | 281 | private onOpen(): void { 282 | this.#backoff.reset(); 283 | this.node.emit(ConnectionEvents.Open); 284 | this._flush() 285 | .then(() => this.configureResuming(this.options.resumeTimeout, this.options.resumeKey)) 286 | .catch((e) => this.node.emit(ConnectionEvents.Error, e)); 287 | } 288 | 289 | private onClose(code: number, reason: string): void { 290 | this.node.emit(ConnectionEvents.Close, code, reason); 291 | this._reconnect(); 292 | } 293 | 294 | private onUpgrade(req: IncomingMessage) { 295 | this.node.emit(ConnectionEvents.Upgrade, req); 296 | } 297 | 298 | private onMessage(d: WebSocket.Data): void { 299 | if (Array.isArray(d)) d = Buffer.concat(d); 300 | else if (d instanceof ArrayBuffer) d = Buffer.from(d); 301 | 302 | let pk: IncomingPayload; 303 | try { 304 | pk = JSON.parse((d as string | Buffer).toString()); 305 | } catch (e) { 306 | this.node.emit(ConnectionEvents.Error, e as Error); 307 | return; 308 | } 309 | 310 | if ('guildId' in pk) this.node.players.get(pk.guildId)?.emit(pk.op, pk); 311 | 312 | // @ts-expect-error `pk` is an union of types, emit expects only one of them at at time. 313 | this.node.emit(pk.op, pk); 314 | } 315 | 316 | private onError(err: Error): void { 317 | this.node.emit(ConnectionEvents.Error, err); 318 | this._reconnect(); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/core/Http.ts: -------------------------------------------------------------------------------- 1 | import { IncomingHttpHeaders, IncomingMessage, request, STATUS_CODES } from 'http'; 2 | import { URL } from 'url'; 3 | import type { BaseNode } from '../base/BaseNode'; 4 | import type { ExceptionSeverity } from '../types/IncomingPayloads'; 5 | import { RoutePlanner } from './RoutePlanner'; 6 | 7 | export class HTTPError extends Error { 8 | public method: string; 9 | public statusCode: number; 10 | public headers: IncomingHttpHeaders; 11 | public path: string; 12 | 13 | public constructor(httpMessage: IncomingMessage, method: string, url: URL) { 14 | super(`${httpMessage.statusCode} ${STATUS_CODES[httpMessage.statusCode as number]}`); 15 | this.statusCode = httpMessage.statusCode as number; 16 | this.headers = httpMessage.headers; 17 | this.name = this.constructor.name; 18 | this.path = url.toString(); 19 | this.method = method; 20 | } 21 | 22 | public get statusMessage() { 23 | return STATUS_CODES[this.statusCode]; 24 | } 25 | } 26 | 27 | export enum LoadType { 28 | /** 29 | * A single track is loaded. 30 | */ 31 | TrackLoaded = 'TRACK_LOADED', 32 | 33 | /** 34 | * A playlist is loaded. 35 | */ 36 | PlaylistLoaded = 'PLAYLIST_LOADED', 37 | 38 | /** 39 | * A search result is made (i.e `ytsearch: some song`). 40 | */ 41 | SearchResult = 'SEARCH_RESULT', 42 | 43 | /** 44 | * No matches/sources could be found for a given identifier. 45 | */ 46 | NoMatches = 'NO_MATCHES', 47 | 48 | /** 49 | * Lavaplayer failed to load something for some reason. 50 | */ 51 | LoadFailed = 'LOAD_FAILED' 52 | } 53 | 54 | /** 55 | * A track response containing all information about the request. 56 | */ 57 | export interface TrackResponse { 58 | /** 59 | * The type of response. 60 | * @example 61 | * ```json 62 | * "TRACK_LOADED" 63 | * ``` 64 | */ 65 | loadType: LoadType; 66 | 67 | /** 68 | * The playlist information. 69 | * @note Only filled when `loadType` is `LoadType.PlaylistLoaded`. 70 | * @example 71 | * ```json 72 | * {} 73 | * ``` 74 | * 75 | * @example 76 | * ```json 77 | * { 78 | * "name": "Example YouTube Playlist", 79 | * "selectedTrack": 3 80 | * } 81 | */ 82 | playlistInfo: PlaylistInfo | {}; 83 | 84 | /** 85 | * The loaded tracks. 86 | * @example 87 | * ```json 88 | * [{ 89 | * "track": "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==", 90 | * "info": { 91 | * "identifier": "dQw4w9WgXcQ", 92 | * "isSeekable": true, 93 | * "author": "RickAstleyVEVO", 94 | * "length": 212000, 95 | * "isStream": false, 96 | * "position": 0, 97 | * "title": "Rick Astley - Never Gonna Give You Up", 98 | * "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 99 | * } 100 | * }] 101 | * ``` 102 | */ 103 | tracks: Track[]; 104 | 105 | /** 106 | * Exception errors. 107 | * @note Only present when `loadType` is `LoadType.LoadFailed`. 108 | */ 109 | exception?: { 110 | /** 111 | * Details why the track failed to load, and is okay to display to end-users. 112 | * @example 113 | * ```json 114 | * "The uploader has not made this video available in your country." 115 | * ``` 116 | */ 117 | message: string; 118 | 119 | /** 120 | * Severity represents how common the error is. A severity level of `COMMON` indicates that the error is 121 | * non-fatal and that the issue is not from Lavalink itself. 122 | */ 123 | severity: ExceptionSeverity; 124 | }; 125 | } 126 | 127 | /** 128 | * The playlist information. 129 | */ 130 | export interface PlaylistInfo { 131 | /** 132 | * The name of the playlist that was loaded. 133 | * @example 134 | * ```json 135 | * "Example YouTube Playlist" 136 | * ``` 137 | */ 138 | name: string; 139 | 140 | /** 141 | * The track that was selected from the playlist. 142 | * @example 143 | * ```json 144 | * 3 145 | * ``` 146 | */ 147 | selectedTrack: number; 148 | } 149 | 150 | export interface TrackInfo { 151 | /** 152 | * The identifier of the track. 153 | * @example 154 | * ```json 155 | * "dQw4w9WgXcQ" 156 | * ``` 157 | */ 158 | identifier: string; 159 | 160 | /** 161 | * Whether or not the track can be seeked. 162 | * @example 163 | * ```json 164 | * true 165 | * ``` 166 | */ 167 | isSeekable: boolean; 168 | 169 | /** 170 | * The author of the track. 171 | * @example 172 | * ```json 173 | * "RickAstleyVEVO" 174 | * ``` 175 | */ 176 | author: string; 177 | 178 | /** 179 | * The length in milliseconds of the track. 180 | * @example 181 | * ```json 182 | * 212000 183 | * ``` 184 | */ 185 | length: number; 186 | 187 | /** 188 | * Whether or not the track is a stream. 189 | * @example 190 | * ```json 191 | * false 192 | * ``` 193 | */ 194 | isStream: boolean; 195 | 196 | /** 197 | * The position at which the track was selected. 198 | * @example 199 | * ```json 200 | * 0 201 | * ``` 202 | */ 203 | position: number; 204 | 205 | /** 206 | * The title of the track. 207 | * @example 208 | * ```json 209 | * "Rick Astley - Never Gonna Give You Up" 210 | * ``` 211 | */ 212 | title: string; 213 | 214 | /** 215 | * The URI of the track. 216 | * @example 217 | * ```json 218 | * "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 219 | * ``` 220 | */ 221 | uri: string; 222 | } 223 | 224 | /** 225 | * The track information. 226 | */ 227 | export interface Track { 228 | /** 229 | * The track's data to be used when interacting with the Lavalink server. 230 | * @note You can use [[Http.decode]] to retrieve the information for the track using this string. 231 | * @example 232 | * ```json 233 | * "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==" 234 | * ``` 235 | */ 236 | track: string; 237 | 238 | /** 239 | * The information for the track. 240 | * @example 241 | * { 242 | * "identifier": "dQw4w9WgXcQ", 243 | * "isSeekable": true, 244 | * "author": "RickAstleyVEVO", 245 | * "length": 212000, 246 | * "isStream": false, 247 | * "position": 0, 248 | * "title": "Rick Astley - Never Gonna Give You Up", 249 | * "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 250 | * } 251 | */ 252 | info: TrackInfo; 253 | } 254 | 255 | export class Http { 256 | public readonly node: BaseNode; 257 | public input: string; 258 | public base: string | undefined; 259 | public routeplanner: RoutePlanner; 260 | 261 | public constructor(node: BaseNode, input: string, base?: string) { 262 | this.node = node; 263 | this.input = input; 264 | this.base = base; 265 | this.routeplanner = new RoutePlanner(this); 266 | } 267 | 268 | public get url() { 269 | return new URL(this.input, this.base); 270 | } 271 | 272 | /** 273 | * Loads a track by its identifier. 274 | * @param identifier The track to be loaded. 275 | */ 276 | public load(identifier: string): Promise { 277 | const { url } = this; 278 | url.pathname = '/loadtracks'; 279 | url.searchParams.append('identifier', identifier); 280 | 281 | return this.do('GET', url); 282 | } 283 | 284 | /** 285 | * Decodes a track. 286 | * @param track The track to be decoded. 287 | * @example 288 | * ```typescript 289 | * const identifier = 'QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA=='; 290 | * 291 | * const track = await http.decode(identifier); 292 | * console.log(track); 293 | * // Logs: { 294 | * // "identifier": "dQw4w9WgXcQ", 295 | * // "isSeekable": true, 296 | * // "author": "RickAstleyVEVO", 297 | * // "length": 212000, 298 | * // "isStream": false, 299 | * // "position": 0, 300 | * // "title": "Rick Astley - Never Gonna Give You Up", 301 | * // "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 302 | * // } 303 | * ``` 304 | */ 305 | public decode(track: string): Promise; 306 | 307 | /** 308 | * Decodes multiple tracks. 309 | * @note This returns an array of [[Track]]s, not a [[TrackInfo]]. 310 | * @param tracks The tracks to be decoded. 311 | */ 312 | public decode(tracks: string[]): Promise; 313 | public decode(tracks: string | string[]): Promise; 314 | public decode(tracks: string | string[]): Promise { 315 | const { url } = this; 316 | if (Array.isArray(tracks)) { 317 | url.pathname = '/decodetracks'; 318 | return this.do('POST', url, Buffer.from(JSON.stringify(tracks))); 319 | } 320 | url.pathname = '/decodetrack'; 321 | url.searchParams.append('track', tracks); 322 | return this.do('GET', url); 323 | } 324 | 325 | public async do(method: string, url: URL, data?: Buffer): Promise { 326 | const message = await new Promise((resolve) => { 327 | const req = request( 328 | { 329 | method, 330 | hostname: url.hostname, 331 | port: url.port, 332 | protocol: url.protocol, 333 | path: url.pathname + url.search, 334 | headers: { 335 | Authorization: this.node.password, 336 | 'Content-Type': 'application/json', 337 | Accept: 'application/json' 338 | } 339 | }, 340 | resolve 341 | ); 342 | 343 | if (data) req.write(data); 344 | req.end(); 345 | }); 346 | 347 | if (message.statusCode && message.statusCode >= 200 && message.statusCode < 300) { 348 | const chunks: Array = []; 349 | for await (const chunk of message) { 350 | chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); 351 | } 352 | 353 | const data = Buffer.concat(chunks); 354 | return JSON.parse(data.toString()); 355 | } 356 | 357 | throw new HTTPError(message, method, url); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/core/Player.ts: -------------------------------------------------------------------------------- 1 | import type { GatewayVoiceStateUpdate } from 'discord-api-types/v9'; 2 | import { EventEmitter } from 'events'; 3 | import type { BaseNode, VoiceServerUpdate, VoiceStateUpdate } from '../base/BaseNode'; 4 | import type { IncomingEventPayload, IncomingPlayerUpdatePayload, IncomingPlayerUpdatePayloadState } from '../types/IncomingPayloads'; 5 | import type { EqualizerBand, OutgoingFilterPayload, OutgoingPayload } from '../types/OutgoingPayloads'; 6 | import type { Track } from './Http'; 7 | 8 | export const enum Status { 9 | Instantiated, 10 | Playing, 11 | Paused, 12 | Ended, 13 | Errored, 14 | Stuck, 15 | Unknown 16 | } 17 | 18 | export interface PlayerOptions { 19 | start?: number; 20 | end?: number; 21 | noReplace?: boolean; 22 | pause?: boolean; 23 | } 24 | 25 | export interface FilterOptions extends Omit {} 26 | 27 | export interface JoinOptions { 28 | mute?: boolean; 29 | deaf?: boolean; 30 | } 31 | 32 | export class Player extends EventEmitter { 33 | public readonly node: T; 34 | public guildID: string; 35 | public status: Status = Status.Instantiated; 36 | private lastPosition: IncomingPlayerUpdatePayloadState | null = null; 37 | 38 | public constructor(node: T, guildID: string) { 39 | super(); 40 | this.node = node; 41 | this.guildID = guildID; 42 | 43 | this.on('event', (d: IncomingEventPayload) => { 44 | switch (d.type) { 45 | case 'TrackStartEvent': 46 | this.status = Status.Playing; 47 | break; 48 | case 'TrackEndEvent': 49 | if (d.reason !== 'REPLACED') this.status = Status.Ended; 50 | break; 51 | case 'TrackExceptionEvent': 52 | this.status = Status.Errored; 53 | break; 54 | case 'TrackStuckEvent': 55 | this.status = Status.Stuck; 56 | break; 57 | case 'WebSocketClosedEvent': 58 | this.status = Status.Ended; 59 | break; 60 | default: 61 | this.status = Status.Unknown; 62 | break; 63 | } 64 | }); 65 | 66 | this.on('playerUpdate', (d: IncomingPlayerUpdatePayload) => { 67 | this.lastPosition = d.state; 68 | }); 69 | } 70 | 71 | public get playing(): boolean { 72 | return this.status === Status.Playing; 73 | } 74 | 75 | public get paused(): boolean { 76 | return this.status === Status.Paused; 77 | } 78 | 79 | public get voiceState(): VoiceStateUpdate | null { 80 | const session = this.node.voiceStates.get(this.guildID); 81 | if (!session) return null; 82 | 83 | return { 84 | ...session, 85 | guild_id: this.guildID, 86 | user_id: this.node.userID 87 | }; 88 | } 89 | 90 | public get voiceServer(): VoiceServerUpdate | null { 91 | return this.node.voiceServers.get(this.guildID) ?? null; 92 | } 93 | 94 | public get position(): number { 95 | // We haven't received data yet so return 0 96 | if (!this.lastPosition) return 0; 97 | // If we are paused don't need to account for time offset. 98 | if (this.paused) return this.lastPosition.position; 99 | // Otherwise we do account for time offset. 100 | return this.lastPosition.position + (Date.now() - this.lastPosition.time); 101 | } 102 | 103 | public async moveTo(node: BaseNode): Promise { 104 | if (this.node === node) return; 105 | 106 | const { voiceState, voiceServer } = this; 107 | if (voiceServer === null || voiceState === null) throw new Error('no voice state/server data to move'); 108 | 109 | await this.destroy(); 110 | await Promise.all([node.voiceStateUpdate(voiceState), node.voiceServerUpdate(voiceServer)]); 111 | } 112 | 113 | public leave(): unknown { 114 | return this.join(null); 115 | } 116 | 117 | public join(channel: string | null, { deaf = false, mute = false }: JoinOptions = {}): unknown { 118 | this.node.voiceServers.delete(this.guildID); 119 | this.node.voiceStates.delete(this.guildID); 120 | 121 | const data: GatewayVoiceStateUpdate = { 122 | op: 4, 123 | d: { 124 | guild_id: this.guildID, 125 | channel_id: channel, 126 | self_deaf: deaf, 127 | self_mute: mute 128 | } 129 | }; 130 | 131 | return this.node.send(this.guildID, data); 132 | } 133 | 134 | public async play(track: string | Track, { start, end, noReplace, pause }: PlayerOptions = {}): Promise { 135 | await this.send({ 136 | op: 'play', 137 | guildId: this.guildID, 138 | track: typeof track === 'object' ? track.track : track, 139 | startTime: start, 140 | endTime: end, 141 | noReplace, 142 | pause 143 | }); 144 | 145 | if (pause) this.status = Status.Paused; 146 | else this.status = Status.Playing; 147 | } 148 | 149 | /** 150 | * Sets the filters for the player. 151 | * @note This is not available in Lavalink v3.3. 152 | * @param options The filters to be sent. 153 | */ 154 | public setFilters(options: FilterOptions): Promise { 155 | return this.send({ 156 | op: 'filters', 157 | guildId: this.guildID, 158 | ...options 159 | }); 160 | } 161 | 162 | /** 163 | * @param volume The new volume to be set. 164 | */ 165 | public setVolume(volume: number): Promise { 166 | return this.send({ 167 | op: 'volume', 168 | guildId: this.guildID, 169 | volume 170 | }); 171 | } 172 | 173 | /** 174 | * @param equalizer The equalizer bads to be set. 175 | */ 176 | public setEqualizer(equalizer: readonly EqualizerBand[]): Promise { 177 | return this.send({ 178 | op: 'equalizer', 179 | guildId: this.guildID, 180 | bands: equalizer 181 | }); 182 | } 183 | 184 | public seek(position: number): Promise { 185 | return this.send({ 186 | op: 'seek', 187 | guildId: this.guildID, 188 | position 189 | }); 190 | } 191 | 192 | public async pause(pause = true): Promise { 193 | await this.send({ 194 | op: 'pause', 195 | guildId: this.guildID, 196 | pause 197 | }); 198 | 199 | if (pause) this.status = Status.Paused; 200 | else this.status = Status.Playing; 201 | } 202 | 203 | public async stop(): Promise { 204 | await this.send({ 205 | op: 'stop', 206 | guildId: this.guildID 207 | }); 208 | 209 | this.status = Status.Ended; 210 | } 211 | 212 | public async destroy(): Promise { 213 | if (this.node.connected) { 214 | await this.send({ 215 | op: 'destroy', 216 | guildId: this.guildID 217 | }); 218 | } 219 | this.status = Status.Ended; 220 | this.node.players.delete(this.guildID); 221 | } 222 | 223 | public voiceUpdate(sessionId: string, event: VoiceServerUpdate): Promise { 224 | return this.send({ 225 | op: 'voiceUpdate', 226 | guildId: this.guildID, 227 | event, 228 | sessionId 229 | }); 230 | } 231 | 232 | public send(data: OutgoingPayload): Promise { 233 | const conn = this.node.connection; 234 | if (conn) return conn.send(data); 235 | return Promise.reject(new Error('no WebSocket connection available')); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/core/PlayerStore.ts: -------------------------------------------------------------------------------- 1 | import type { BaseNode } from '../base/BaseNode'; 2 | import { Player } from './Player'; 3 | 4 | /** 5 | * Represents a collection of players. 6 | */ 7 | export class PlayerStore extends Map> { 8 | /** 9 | * The [[Node]] or [[ClusterNode]] that created this store. 10 | */ 11 | public readonly node: T; 12 | 13 | public constructor(node: T) { 14 | super(); 15 | this.node = node; 16 | } 17 | 18 | /** 19 | * Gets an existing player, creating a new one if none was found. 20 | * @param key The guild's ID to get a player from. 21 | */ 22 | public get(key: string): Player { 23 | let player = super.get(key); 24 | if (!player) { 25 | player = new Player(this.node, key); 26 | this.set(key, player); 27 | } 28 | 29 | return player; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/core/RoutePlanner.ts: -------------------------------------------------------------------------------- 1 | import type { Http } from './Http'; 2 | 3 | export type RoutePlannerStatus = RotatingIpRoutePlanner | NanoIpRoutePlanner | RotatingIpRoutePlanner; 4 | 5 | export interface BaseRoutePlannerStatusDetails { 6 | ipBlock: { 7 | type: string; 8 | size: string; 9 | }; 10 | failingAddresses: { 11 | address: string; 12 | failingTimestamp: number; 13 | failingTime: string; 14 | }[]; 15 | } 16 | 17 | export interface RotatingIpRoutePlanner { 18 | class: 'RotatingIpRoutePlanner'; 19 | details: BaseRoutePlannerStatusDetails & { 20 | rotateIndex: string; 21 | ipIndex: string; 22 | currentAddress: string; 23 | }; 24 | } 25 | 26 | export interface NanoIpRoutePlanner { 27 | class: 'NanoIpRoutePlanner'; 28 | details: BaseRoutePlannerStatusDetails & { 29 | currentAddressIndex: number; 30 | }; 31 | } 32 | 33 | export interface RotatingNanoIpRoutePlanner { 34 | class: 'RotatingNanoIpRoutePlanner'; 35 | details: BaseRoutePlannerStatusDetails & { 36 | blockIndex: string; 37 | currentAddressIndex: number; 38 | }; 39 | } 40 | 41 | export class RoutePlanner { 42 | public constructor(public readonly http: Http) {} 43 | 44 | public status(): Promise { 45 | const { url } = this.http; 46 | url.pathname = '/routeplanner/status'; 47 | return this.http.do('get', url); 48 | } 49 | 50 | public unmark(address?: string): Promise { 51 | const { url } = this.http; 52 | if (address) { 53 | url.pathname = '/routeplanner/free/address'; 54 | return this.http.do('post', url, Buffer.from(JSON.stringify({ address }))); 55 | } 56 | 57 | url.pathname = '/routeplanner/free/all'; 58 | return this.http.do('post', url); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base/BaseCluster'; 2 | export * from './base/BaseNode'; 3 | export * from './Cluster'; 4 | export * from './ClusterNode'; 5 | export * from './core/Connection'; 6 | export * from './core/Http'; 7 | export * from './core/Player'; 8 | export * from './core/PlayerStore'; 9 | export * from './core/RoutePlanner'; 10 | export * from './Node'; 11 | export * from './types/OutgoingPayloads'; 12 | export * from './types/IncomingPayloads'; 13 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "../dist", 6 | "tsBuildInfoFile": "../dist/.tsbuildinfo", 7 | "composite": true 8 | }, 9 | "include": ["."] 10 | } 11 | -------------------------------------------------------------------------------- /src/types/IncomingPayloads.ts: -------------------------------------------------------------------------------- 1 | export type IncomingPayload = IncomingPlayerUpdatePayload | IncomingStatsPayload | IncomingEventPayload; 2 | 3 | export interface IncomingPlayerUpdatePayloadState { 4 | time: number; 5 | position: number; 6 | } 7 | 8 | export interface IncomingPlayerUpdatePayload { 9 | op: 'playerUpdate'; 10 | guildId: string; 11 | state: IncomingPlayerUpdatePayloadState; 12 | } 13 | 14 | export interface IncomingStatsPayloadMemory { 15 | free: number; 16 | used: number; 17 | allocated: number; 18 | reservable: number; 19 | } 20 | 21 | export interface IncomingStatsPayloadCPU { 22 | cores: number; 23 | systemLoad: number; 24 | lavalinkLoad: number; 25 | } 26 | 27 | export interface IncomingStatsPayloadFrames { 28 | sent: number; 29 | nulled: number; 30 | deficit: number; 31 | } 32 | 33 | export interface IncomingStatsPayload { 34 | op: 'stats'; 35 | players: number; 36 | playingPlayers: number; 37 | uptime: number; 38 | memory: IncomingStatsPayloadMemory; 39 | cpu: IncomingStatsPayloadCPU; 40 | frames?: IncomingStatsPayloadFrames; 41 | } 42 | 43 | interface IIncomingEvent { 44 | op: 'event'; 45 | guildId: string; 46 | } 47 | 48 | export type IncomingEventPayload = 49 | | IncomingEventStartPayload 50 | | IncomingEventTrackEndPayload 51 | | IncomingEventTrackExceptionPayload 52 | | IncomingEventTrackStuckPayload 53 | | IncomingEventWebSocketClosedPayload; 54 | 55 | export interface IncomingEventStartPayload extends IIncomingEvent { 56 | type: 'TrackStartEvent'; 57 | track: string; 58 | } 59 | 60 | export interface IncomingEventTrackEndPayload extends IIncomingEvent { 61 | type: 'TrackEndEvent'; 62 | track: string; 63 | reason: string; 64 | } 65 | 66 | export interface IncomingEventTrackExceptionPayloadException { 67 | /** 68 | * The message explaining the cause of the exception. 69 | * @example 70 | * ```json 71 | * "The uploader has not made this video available in your country." 72 | * ``` 73 | */ 74 | message: string; 75 | 76 | /** 77 | * The severity of the exception. 78 | * @example 79 | * ```json 80 | * "COMMON" 81 | * ``` 82 | */ 83 | severity: ExceptionSeverity; 84 | 85 | /** 86 | * The cause for the exception. 87 | */ 88 | cause: string; 89 | } 90 | 91 | export interface IncomingEventTrackExceptionPayload extends IIncomingEvent { 92 | type: 'TrackExceptionEvent'; 93 | 94 | /** 95 | * The track that received the exception. 96 | */ 97 | track: string; 98 | 99 | /** 100 | * The exception's details. 101 | */ 102 | exception: IncomingEventTrackExceptionPayloadException; 103 | } 104 | 105 | export interface IncomingEventTrackStuckPayload extends IIncomingEvent { 106 | type: 'TrackStuckEvent'; 107 | 108 | /** 109 | * The track that got stuck. 110 | */ 111 | track: string; 112 | 113 | /** 114 | * The threshold in milliseconds at which the track will resume. 115 | */ 116 | thresholdMs: number; 117 | } 118 | 119 | export interface IncomingEventWebSocketClosedPayload extends IIncomingEvent { 120 | type: 'WebSocketClosedEvent'; 121 | 122 | /** 123 | * The closing error code from the websocket. 124 | * @example 125 | * ```json 126 | * 4006 127 | * ``` 128 | */ 129 | code: number; 130 | 131 | /** 132 | * The reason the websocket was closed. 133 | * @example 134 | * ```json 135 | * "Your session is no longer valid." 136 | * ``` 137 | */ 138 | reason: string; 139 | 140 | /** 141 | * Whether or not the websocket was closed by Discord. 142 | * @example 143 | * ```json 144 | * true 145 | * ``` 146 | */ 147 | byRemote: boolean; 148 | } 149 | 150 | export const enum ExceptionSeverity { 151 | /** 152 | * The cause is known and expected, indicates that there is nothing wrong with the library itself. 153 | */ 154 | Common = 'COMMON', 155 | 156 | /** 157 | * The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside 158 | * service responds in a format that we do not expect. 159 | */ 160 | Suspicious = 'SUSPICIOUS', 161 | 162 | /** 163 | * If the probable cause is an issue with the library or when there is no way to tell what the cause might be. 164 | * This is the default level and other levels are used in cases where the thrower has more in-depth knowledge 165 | * about the error. 166 | */ 167 | Fault = 'FAULT' 168 | } 169 | -------------------------------------------------------------------------------- /src/types/OutgoingPayloads.ts: -------------------------------------------------------------------------------- 1 | import type { VoiceServerUpdate } from '../base/BaseNode'; 2 | 3 | export type OutgoingPayload = 4 | | OutgoingDestroyPayload 5 | | OutgoingEqualizerPayload 6 | | OutgoingPausePayload 7 | | OutgoingPlayPayload 8 | | OutgoingSeekPayload 9 | | OutgoingStopPayload 10 | | OutgoingVoiceUpdatePayload 11 | | OutgoingVolumePayload 12 | | OutgoingConfigureResumingPayload 13 | | OutgoingFilterPayload; 14 | 15 | export interface BaseOutgoingPayload { 16 | /** 17 | * The guild's ID to identify the player. 18 | */ 19 | guildId: string; 20 | } 21 | 22 | export interface OutgoingDestroyPayload extends BaseOutgoingPayload { 23 | op: 'destroy'; 24 | } 25 | 26 | export interface OutgoingStopPayload extends BaseOutgoingPayload { 27 | op: 'stop'; 28 | } 29 | 30 | export interface OutgoingSeekPayload extends BaseOutgoingPayload { 31 | op: 'seek'; 32 | 33 | /** 34 | * The offset in milliseconds to play the current track from. 35 | */ 36 | position: number; 37 | } 38 | 39 | export interface OutgoingPausePayload extends BaseOutgoingPayload { 40 | op: 'pause'; 41 | 42 | /** 43 | * Whether or not the player should be paused. 44 | */ 45 | pause: boolean; 46 | } 47 | 48 | export interface OutgoingPlayPayload extends BaseOutgoingPayload { 49 | op: 'play'; 50 | 51 | /** 52 | * The track to be played. 53 | */ 54 | track: string; 55 | 56 | /** 57 | * If set to true, this operation will be ignored if a track is already playing or paused. 58 | */ 59 | noReplace?: boolean; 60 | 61 | /** 62 | * Determines the number of milliseconds to offset the track by. Defaults to 0. 63 | */ 64 | startTime?: number; 65 | 66 | /** 67 | * Determines at the number of milliseconds at which point the track should stop playing. Helpful if you only want 68 | * to play a snippet of a bigger track. By default the track plays until it's end as per the encoded data. 69 | */ 70 | endTime?: number; 71 | 72 | /** 73 | * If set to true, the playback will be paused. 74 | */ 75 | pause?: boolean; 76 | } 77 | 78 | export interface OutgoingVoiceUpdatePayload extends BaseOutgoingPayload { 79 | op: 'voiceUpdate'; 80 | 81 | /** 82 | * The voice channel's session ID. 83 | */ 84 | sessionId: string; 85 | 86 | /** 87 | * The raw event data from Discord. 88 | */ 89 | event: VoiceServerUpdate; 90 | } 91 | 92 | export interface OutgoingVolumePayload extends BaseOutgoingPayload { 93 | op: 'volume'; 94 | 95 | /** 96 | * The volume to be set. 97 | * @default 100 98 | * @range [0, 1000] 99 | */ 100 | volume: number; 101 | } 102 | 103 | export interface EqualizerBand { 104 | /** 105 | * The band to be changed, ranges from 0 to 14 inclusive. 106 | * @range [0, 14] 107 | */ 108 | band: number; 109 | 110 | /** 111 | * The multiplier of the band. Valid values range from -0.25 to 1.0, where -0.25 means the given band is 112 | * completely muted, and 0.25 means it is doubled. Modifying the gain could also change the volume of the output. 113 | * @default 0 114 | * @range [-0.25, 1] 115 | */ 116 | gain: number; 117 | } 118 | 119 | export interface OutgoingEqualizerPayload extends BaseOutgoingPayload { 120 | op: 'equalizer'; 121 | 122 | /** 123 | * The bands to be set. 124 | */ 125 | bands: readonly EqualizerBand[]; 126 | } 127 | 128 | export interface OutgoingConfigureResumingPayload { 129 | op: 'configureResuming'; 130 | 131 | /** 132 | * The string you will need to send when resuming the session. Set to null to disable resuming altogether. 133 | */ 134 | key?: string | null; 135 | 136 | /** 137 | * The number of seconds after disconnecting before the session is closed anyways. 138 | * This is useful for avoiding accidental leaks. 139 | */ 140 | timeout?: number; 141 | } 142 | 143 | /** 144 | * @note This is not available in Lavalink v3.3. 145 | */ 146 | export interface OutgoingFilterPayload extends BaseOutgoingPayload { 147 | op: 'filters'; 148 | 149 | /** 150 | * The volume to set the track. Valid values range from 0 to 5.0, where 0 means the stream is completely muted, and 151 | * 2 means it is doubled. 152 | * @range [0, 5] 153 | */ 154 | volume?: number; 155 | 156 | /** 157 | * The equalizer bands, there are 15 bands (0-14) that can be changed. 158 | */ 159 | equalizer?: readonly EqualizerBand[]; 160 | 161 | /** 162 | * The karaoke options, uses equalization to eliminate a part of a band, usually targeting vocals. 163 | */ 164 | karaoke?: KaraokeOptions; 165 | 166 | /** 167 | * The timescale options, used to change the speed, pitch, and rate. 168 | */ 169 | timescale?: TimescaleOptions; 170 | 171 | /** 172 | * The tremolo options, uses amplification to create a shuddering effect, where the volume quickly oscillates, 173 | * {@link https://en.wikipedia.org/wiki/File:Fuse_Electronics_Tremolo_MK-III_Quick_Demo.ogv example}. 174 | */ 175 | tremolo?: FrequencyDepthOptions; 176 | 177 | /** 178 | * The vibrato options. Similar to tremolo, while tremolo oscillates the volume, vibrato oscillates the pitch. 179 | */ 180 | vibrato?: FrequencyDepthOptions; 181 | 182 | /** 183 | * The distortion options. 184 | */ 185 | distortion?: DistortionOptions; 186 | 187 | /** 188 | * The rotation options. This rotates the sound around the stereo channels/user headphones, also known as 189 | * {@link https://en.wikipedia.org/wiki/Panning_(audio) Audio Panning}. 190 | */ 191 | rotation?: RotationOptions; 192 | 193 | /** 194 | * The channel mix options. 195 | */ 196 | channelMix?: ChannelMixOptions; 197 | 198 | /** 199 | * The low pass options. 200 | */ 201 | lowPass?: LowPassOptions; 202 | } 203 | 204 | /** 205 | * @note This is not available in Lavalink v3.3. 206 | */ 207 | export interface KaraokeOptions { 208 | /** 209 | * The level. 210 | * @default 1.0 211 | */ 212 | level?: number; 213 | 214 | /** 215 | * The mono level. 216 | * @default 1.0 217 | */ 218 | monoLevel?: number; 219 | 220 | /** 221 | * The band to filter. 222 | * @default 220.0 223 | */ 224 | filterBand?: number; 225 | 226 | /** 227 | * The width of the frequencies to filter. 228 | * @default 100.0 229 | */ 230 | filterWidth?: number; 231 | } 232 | 233 | /** 234 | * @note This is not available in Lavalink v3.3. 235 | */ 236 | export interface TimescaleOptions { 237 | /** 238 | * The speed of the track. Must be >=0. 239 | * @default 1.0 240 | */ 241 | speed?: number; 242 | 243 | /** 244 | * The pitch of the track. Must be >=0. 245 | * @default 1.0 246 | */ 247 | pitch?: number; 248 | 249 | /** 250 | * The rate of the track. Must be >=0. 251 | * @default 1.0 252 | */ 253 | rate?: number; 254 | } 255 | 256 | /** 257 | * @note This is not available in Lavalink v3.3. 258 | */ 259 | export interface FrequencyDepthOptions { 260 | /** 261 | * The frequency to edit. Must be >0 and <=14. 262 | * @default 2.0 263 | */ 264 | frequency?: number; 265 | 266 | /** 267 | * The depth for the selected frequency. Must be >0 and <=1. 268 | * @default 0.5 269 | */ 270 | depth?: number; 271 | } 272 | 273 | /** 274 | * @note This is not available in Lavalink v3.3. 275 | */ 276 | export interface DistortionOptions { 277 | /** 278 | * The sine's offset. 279 | * @default 0.0 280 | */ 281 | sinOffset?: number; 282 | 283 | /** 284 | * The sine's scale. 285 | * @default 1.0 286 | */ 287 | sinScale?: number; 288 | 289 | /** 290 | * The cosine's offset. 291 | * @default 0.0 292 | */ 293 | cosOffset?: number; 294 | 295 | /** 296 | * The cosine's scale. 297 | * @default 1.0 298 | */ 299 | cosScale?: number; 300 | 301 | /** 302 | * The tangent offset. 303 | * @default 0.0 304 | */ 305 | tanOffset?: number; 306 | 307 | /** 308 | * The tangent scale. 309 | * @default 1.0 310 | */ 311 | tanScale?: number; 312 | 313 | /** 314 | * The overall offset for all waves. 315 | * @default 0.0 316 | */ 317 | offset?: number; 318 | 319 | /** 320 | * The overall scale for all waves. 321 | * @default 1.0 322 | */ 323 | scale?: number; 324 | } 325 | 326 | /** 327 | * @note This is not available in Lavalink v3.3. 328 | */ 329 | export interface RotationOptions { 330 | /** 331 | * The frequency in Hz to rotate. 332 | * @default 2.0 333 | */ 334 | rotationHz?: number; 335 | } 336 | 337 | /** 338 | * Mixes both channels (left and right), with a configurable factor on how much each channel affects the other. 339 | * With the defaults, both channels are kept independent from each other. 340 | * @note Setting all factors to 0.5 means both channels get the same audio. 341 | * @note This is not available in Lavalink v3.3. 342 | */ 343 | export interface ChannelMixOptions { 344 | /** 345 | * The left-to-left mix, modifies the volume in the left channel. 346 | * @default 1.0 347 | */ 348 | leftToLeft: number; 349 | 350 | /** 351 | * The left-to-right mix, modifies how much of the left channel goes to the right one. 352 | * @default 0.0 353 | */ 354 | leftToRight: number; 355 | 356 | /** 357 | * The right-to-left mix, modifies how much of the right channel goes to the left one. 358 | * @default 0.0 359 | */ 360 | rightToLeft: number; 361 | 362 | /** 363 | * The right-to-right mix, modifies the volume in the right channel. 364 | * @default 1.0 365 | */ 366 | rightToRight: number; 367 | } 368 | 369 | /** 370 | * Supresses high frequencies given a frequency. 371 | * @note This is not available in Lavalink v3.3. 372 | */ 373 | export interface LowPassOptions { 374 | /** 375 | * The amount of smoothing done. 376 | */ 377 | smoothing: number; 378 | } 379 | -------------------------------------------------------------------------------- /tests/Cluster.test.ts.temp: -------------------------------------------------------------------------------- 1 | import { Client as Gateway } from '@spectacles/gateway'; 2 | import { inspect } from 'util'; 3 | import { Cluster } from '../src'; 4 | 5 | if (!process.env.TOKEN) throw new Error('token not provided'); 6 | if (!process.env.USER_ID) throw new Error('user id not provided'); 7 | 8 | const gateway = new Gateway(process.env.TOKEN); 9 | const cluster = new Cluster({ 10 | nodes: [ 11 | { 12 | password: 'youshallnotpass', 13 | userID: process.env.USER_ID, 14 | host: 'localhost:8080' 15 | }, 16 | { 17 | password: 'youshallnotpass', 18 | userID: process.env.USER_ID, 19 | host: 'localhost:8081' 20 | } 21 | ], 22 | send(_guildID, packet) { 23 | const conn = gateway.connections.get(0); 24 | if (conn) return conn.send(packet); 25 | throw new Error('no gateway connection available'); 26 | }, 27 | filter() { 28 | // return node.tags.includes(client.guilds.get(guildID).region)); 29 | return true; 30 | } 31 | }); 32 | 33 | gateway.on('READY', console.log); 34 | 35 | gateway.on('MESSAGE_CREATE', async (shard, m) => { 36 | console.log(m.content); 37 | if (m.content === 'join') await cluster.get('281630801660215296').join('281630801660215297'); 38 | if (m.content === 'leave') await cluster.get('281630801660215296').leave(); 39 | if (m.content === 'pause') await cluster.get('281630801660215296').pause(); 40 | if (m.content === 'destroy') await cluster.get('281630801660215296').destroy(); 41 | 42 | if (m.content === 'decode') { 43 | const trackResponse = await cluster 44 | .get('281630801660215296') 45 | .node.load('https://www.youtube.com/playlist?list=PLe8jmEHFkvsaDOOWcREvkgFoj6MD0pQ67'); 46 | const decoded = await cluster.get('281630801660215296').node.decode(trackResponse.tracks.map((t) => t.track)); 47 | console.log(decoded.every((e, i) => typeof e === 'object')); 48 | } 49 | 50 | if (m.content === 'play') { 51 | const trackResponse = await cluster 52 | .get('281630801660215296') 53 | .node.load('https://www.youtube.com/playlist?list=PLe8jmEHFkvsaDOOWcREvkgFoj6MD0pQ67'); 54 | void cluster.get('281630801660215296').play(trackResponse.tracks[0]); 55 | } 56 | 57 | if (m.content === 'stats') { 58 | console.log(cluster.nodes.map((node) => node.stats)); 59 | } 60 | 61 | if (m.content === 'reconnect') { 62 | const conn = gateway.connections.get(0); 63 | if (conn) conn.reconnect(); 64 | } 65 | console.log('finished'); 66 | }); 67 | 68 | gateway.on('VOICE_STATE_UPDATE', (shard, s) => cluster.voiceStateUpdate(s)); 69 | gateway.on('VOICE_SERVER_UPDATE', (shard, s) => cluster.voiceServerUpdate(s)); 70 | gateway.on('close', console.log); 71 | gateway.on('error', (shard, err) => console.log(inspect(err, { depth: 2 }))); 72 | 73 | void (async () => { 74 | try { 75 | await gateway.spawn(); 76 | } catch (e) { 77 | console.error(e); 78 | } 79 | })(); 80 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { BaseCluster, BaseNode, Cluster, Node } from '../src'; 2 | 3 | describe('Lavalink', () => { 4 | test('Node extends BaseNode', () => { 5 | expect(Node.prototype instanceof BaseNode).toBe(true); 6 | }); 7 | 8 | test('Cluster extends BaseCluster', () => { 9 | expect(Cluster.prototype instanceof BaseCluster).toBe(true); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/index.test.ts.temp: -------------------------------------------------------------------------------- 1 | import { Client as Gateway } from '@spectacles/gateway'; 2 | import { inspect } from 'util'; 3 | import { Node } from '../src'; 4 | 5 | if (!process.env.TOKEN) throw new Error('token not provided'); 6 | if (!process.env.USER_ID) throw new Error('user id not provided'); 7 | 8 | const gateway = new Gateway(process.env.TOKEN); 9 | const client = new Node({ 10 | password: 'youshallnotpass', 11 | userID: process.env.USER_ID, 12 | hosts: { 13 | rest: 'http://localhost:8081', 14 | ws: 'ws://localhost:8081' 15 | }, 16 | send(_guild, packet) { 17 | const conn = gateway.connections.get(0); 18 | if (conn) return conn.send(packet); 19 | throw new Error('no gateway connection available'); 20 | } 21 | }); 22 | 23 | gateway.on('READY', console.log); 24 | client.on('event', console.log); 25 | 26 | gateway.on('MESSAGE_CREATE', async (shard, m) => { 27 | console.log(m.content); 28 | 29 | const player = client.players.get('281630801660215296'); 30 | if (m.content === 'join') await player.join('281630801660215297'); 31 | if (m.content === 'leave') await player.leave(); 32 | if (m.content === 'pause') await player.pause(); 33 | 34 | if (m.content === 'decode') { 35 | const trackResponse = await client.load('https://www.youtube.com/playlist?list=PLe8jmEHFkvsaDOOWcREvkgFoj6MD0pQ67'); 36 | const decoded = await client.decode(trackResponse.tracks.map((t) => t.track)); 37 | console.log(decoded.every((e, i) => typeof e === 'object')); 38 | } 39 | 40 | if (m.content === 'play') { 41 | const trackResponse = await client.load('https://www.youtube.com/playlist?list=PLe8jmEHFkvsaDOOWcREvkgFoj6MD0pQ67'); 42 | void client.players.get('281630801660215296').play(trackResponse.tracks[0]); 43 | } 44 | 45 | if (m.content.startsWith('eval')) console.log(eval(m.content.slice(4).trim())); 46 | 47 | if (m.content === 'reconnect') { 48 | const conn = gateway.connections.get(0); 49 | if (conn) conn.reconnect(); 50 | } 51 | console.log('finished'); 52 | }); 53 | 54 | gateway.on('VOICE_STATE_UPDATE', (shard, s) => client.voiceStateUpdate(s)); 55 | gateway.on('VOICE_SERVER_UPDATE', (shard, s) => client.voiceServerUpdate(s)); 56 | gateway.on('close', console.log); 57 | gateway.on('error', (shard, err) => console.log(inspect(err, { depth: 2 }))); 58 | 59 | let i = 0; 60 | client.on('error', (e) => console.error(i++, e)); 61 | client.on('open', () => console.log('ll open')); 62 | client.on('close', () => console.log('ll close')); 63 | 64 | void (async () => { 65 | try { 66 | await gateway.spawn(); 67 | } catch (e) { 68 | console.error(e); 69 | } 70 | })(); 71 | -------------------------------------------------------------------------------- /tests/meta/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM adoptopenjdk/openjdk13-openj9:alpine-jre 2 | 3 | WORKDIR /opt/Lavalink 4 | 5 | COPY Lavalink.jar Lavalink.jar 6 | COPY application.yml ./ 7 | 8 | EXPOSE 2333 9 | 10 | CMD ["java", "-jar", "Lavalink.jar"] -------------------------------------------------------------------------------- /tests/meta/Lavalink.jar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a5d8e5b1be2b5b82ed7f0e508ecadf6c7430297937caa92b0fcc3e74d23529ef 3 | size 40394550 4 | -------------------------------------------------------------------------------- /tests/meta/application.yml: -------------------------------------------------------------------------------- 1 | # Rename `application.example.yml` to `application.yml` for usage 2 | 3 | server: # REST and WS server 4 | port: 2333 5 | address: 0.0.0.0 6 | 7 | spring: 8 | main: 9 | banner-mode: log 10 | 11 | lavalink: 12 | server: 13 | password: 'skyra' 14 | sources: 15 | youtube: true 16 | bandcamp: true 17 | soundcloud: true 18 | twitch: true 19 | vimeo: true 20 | mixer: true 21 | http: false 22 | local: false 23 | bufferDurationMs: 400 24 | youtubePlaylistLoadLimit: 6 25 | youtubeSearchEnabled: true 26 | soundcloudSearchEnabled: true 27 | gc-warnings: true 28 | 29 | metrics: 30 | prometheus: 31 | enabled: false 32 | endpoint: /metrics 33 | -------------------------------------------------------------------------------- /tests/meta/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | services: 3 | ll1: 4 | build: . 5 | ports: 6 | - '8080:8080' 7 | - '8081:8081' 8 | ll2: 9 | build: . 10 | ports: 11 | - '8082:8080' 12 | - '8083:8081' 13 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "baseUrl": "." 6 | }, 7 | "include": [".", "../src/**/*"], 8 | "references": [{ "path": "../src" }] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sapphire/ts-config" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src", "tests", "jest.config.ts", "scripts"] 4 | } 5 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "out": "./docs/", 3 | "readme": "./README.md", 4 | "name": "@skyra/audio", 5 | "entryPoints": ["src/index.ts"], 6 | "excludeExternals": true, 7 | "tsconfig": "./src/tsconfig.json" 8 | } 9 | --------------------------------------------------------------------------------